AwsS3Form

2  

extends NPM:MPBasic

4  
5  
6  
7  

Exports: Class

Generate a signed and reday to use formdata to put files to s3 directly from teh browser. Signing is done by using AWS Signature Version 4.

npm modules

12  
13  _ = require('lodash')
14  uuid = require('node-uuid')
15  mime = require('mime-nofs')
16  
17  
18  

internal modules Utils

18  
19  utils = require( "./utils" )
20  
21  

CryptoAdapter

20  
21  cryptoAdapter = require( "./cryptoAdapter" )
22  
23  class AwsS3Form extends require( "mpbasic" )()
24  
25    validation:
26      cryptoModules: [ "crypto", "crypto-js" ]
27      acl: [ "private", "public-read", "public-read-write", "authenticated-read", "bucket-owner-read", "bucket-owner-full-control" ]
28      successActionStatus: [200, 201, 204]
29  
30  
31  

defaults

30  
31    defaults: =>
32      @extend super,
33  
34  

AwsS3Form.accessKeyId String AWS access key

33  
34        accessKeyId: "set-in-config-json"
35  
36  

AwsS3Form.secretAccessKey String AWS access secret

35  
36        secretAccessKey: "set-in-config-json"
37  
38  

AwsS3Form.region String AWS region

37  
38        region: "eu-central-1"
39  
40  

AwsS3Form.bucket String AWS bucket name

39  
40        bucket: null
41  
42  

AwsS3Form.secure Boolean Define if the action uses ssl. true = "https"; false = "http"

41  
42        secure: true
43  
44  

AwsS3Form.redirectUrlTemplate String|Function a redirect url template.

43  
44        redirectUrlTemplate: null
45  
46  

AwsS3Form.redirectUrlTemplate Number HTTP code to return when no redirectUrlTemplate is defined.

45  
46        successActionStatus: 204
47  
48  

AwsS3Form.policyExpiration Date|Number Add time in seconds to now to define the expiration of the policy. Or set a hard Date.

47  
48        policyExpiration: 60*60*12 # Default 12 hrs
49  
50  

AwsS3Form.keyPrefix String Key prefix to define a policy that the key has to start with this value

49  
50        keyPrefix: ""
51  
52  

AwsS3Form.acl String The standard acl type. Only public-read and authenticated-read are allowed

51  
52        acl: "public-read"
53  
54  

AwsS3Form.useUuid Boolean Use a uuid for better security

53  
54        useUuid: true
55  
56  

AwsS3Form.cryptoModule String( enum: crypto|crypto-js) You can switch between the node internal crypo-module or the browser module cryptojs

55  
56        cryptoModule: "crypto"
57      
58  
59  

initialize

basic.initialize()

initialize the module and set the crypto adapter

API
private
66  
67    initialize: ->
68      @_setCryptoModule( @config.cryptoModule )
69      return
70  
71      
72  
73  

create

basic.create( filename [, options ] )

create a new form object

Params
filename String The S3 file key/filename to use.
[options] Object Create options
[options.acl] String Option to overwrite the general acl
[options.secure] String Option to overwrite the general secure
[options.keyPrefix] String Option to overwrite the general keyPrefix
[options.contentType] String Boolean Option to set the content type of the uploaded file. This could be a string with a fixed mime or a boolean to decide if the mime will be guessed by the filename.
[options.redirectUrlTemplate] String Option to overwrite the general redirectUrlTemplate
[options.successActionStatus] String Option to overwrite the general successActionStatus
[options.policyExpiration] Number Date Option to overwrite the general policyExpiration
API
public
90  
91    create: ( filename, options = {} )=>
92  
93  

contentType = mime.lookup( filename )

92  
93  
94      options.now = new Date()
95      if @config.useUuid
96        options.uuid = uuid.v4()
97      
98      if  options.contentType? and _.isString( options.contentType )
99        _cType = options.contentType
100      else if options.contentType
101        _cType = mime.lookup( filename )
102      
103      _data =
104        acl: @_acl( options.acl )
105        credential: @_createCredential( options.now )
106        amzdate: @_shortDate( options.now )
107        contentType: _cType
108  
109      if options.redirectUrlTemplate?
110        _data.success_action_redirect = @_redirectUrl( options.redirectUrlTemplate, filename: filename )
111      else
112        if _.isString( options.successActionStatus )
113          options.successActionStatus = parseInt( options.successActionStatus, 10 )
114        _data.success_action_status = @_successActionStatus( options.successActionStatus )
115  
116      _policyB64 = @_obj2b64( @policy( filename, options, _data ) )
117  
118      _signature = @sign( _policyB64, options )
119  
120      if options.secure?
121        _secure = options.secure
122      else
123        _secure = @config.secure
124  
125      data =
126        action: "#{ if _secure then "https" else "http" }://s3-#{@config.region}.amazonaws.com/#{ @config.bucket }"
127        filefield: "file"
128        fields:
129          key: "#{( options.keyPrefix or @config.keyPrefix )}#{filename}"
130          acl: _data.acl
131          "X-Amz-Credential": _data.credential
132          "X-Amz-Algorithm": "AWS4-HMAC-SHA256"
133          "X-Amz-Date": _data.amzdate
134          "Policy": _policyB64
135          "X-Amz-Signature": _signature.toString()
136  
137      if options.redirectUrlTemplate?
138        data.fields.success_action_redirect = @_redirectUrl( options.redirectUrlTemplate, filename: filename )
139      else
140        data.fields.success_action_status = @_successActionStatus( options.successActionStatus )
141  
142      if options.uuid?
143        data.fields[ "x-amz-meta-uuid" ] = options.uuid
144      
145      if _cType?
146        data.fields[ "Content-Type" ] = _cType
147  
148      return data
149  
150  
151  

policy

basic.policy( filename [, options ] )

Create a new policy object based on AWS Signature Version 4.

Params
filename String The S3 file key/filename to use.
[options] Object Policy options
[options.now] String The current date-time for this policy
[options.uuid] String The uuid to add to the policy
[options.acl] String Option to overwrite the general acl
[options.keyPrefix] String Option to overwrite the general keyPrefix
[options.customConditions] Array Option to set s3 upload conditions. For details see http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html
[options.redirectUrlTemplate] String Option to overwrite the general redirectUrlTemplate
[options.policyExpiration] Number Date Option to overwrite the general policyExpiration
API
public
168  
169    policy: ( filename, options = {}, _predef = {} )=>
170      _date = options.now or new Date()
171  
172      policy =
173        expiration: @_calcDate( options.policyExpiration or @config.policyExpiration, _date )
174        conditions: [
175          { "bucket": @config.bucket }
176          [ "starts-with", "$key", ( options.keyPrefix or @config.keyPrefix ) ]
177          { "acl": _predef.acl or @_acl( options.acl ) }
178          { "x-amz-credential": _predef.credential or @_createCredential( _date ) }
179          { "x-amz-algorithm": "AWS4-HMAC-SHA256" }
180          { "x-amz-date": _predef.amzdate or @_shortDate( _date ) }
181  
182  

[ "starts-with", "$Content-Type", contentType ] ["content-length-range", 0, @settings.maxFileSize ]

182  
183        ]
184  
185      if _predef.success_action_status?
186        policy.conditions.push { "success_action_status": _predef.success_action_status.toString()}
187      else
188        policy.conditions.push { "success_action_redirect": _predef.success_action_redirect or @_redirectUrl( options.redirectUrlTemplate, filename: filename ) }
189      
190      _ctypeCondition = false
191      if options.customConditions?
192        for ccond in options.customConditions
193          policy.conditions.push ccond
194          if ( _.isArray( ccond ) and _.isString( ccond[ 1 ] ) and ccond[ 1 ].toLowerCase() is "$content-type" ) or ( _.isObject( ccond ) and ccond["content-type"]? )
195            _ctypeCondition = true
196      
197      if not _ctypeCondition and _predef?.contentType?
198        policy.conditions.push { "content-type": _predef.contentType }
199        
200      @debug "generated policy", policy
201      if options.uuid?
202        policy.conditions.push { "x-amz-meta-uuid": options.uuid }
203  
204      return policy
205  
206  
207  

sign

basic.sign( policyB64 [, options ] )

Create a AWS Signature Version 4. This is used to create the signature out of the policy.

Params
policyB64 String Base64 encoded policy
[options] Object sign options
[options.now=`new String Date()`] The current date-time for this signature
[options.signdate=converted String options.now] signature date
[options.secretAccessKey] String Change the configured standard secretAccessKey type.
[options.region] String Option to overwrite the general region
API
public
221  
222    sign: ( policyB64, options )=>
223      _date = options.now or new Date()
224      _signdate = options.signdate or @_shortDate( _date, true )
225  
226      _h1 = cryptoAdapter.hmacSha256( "AWS4" + ( options.secretAccessKey or @config.secretAccessKey ), _signdate, "utf8" )
227      _h2 = cryptoAdapter.hmacSha256( _h1, ( options.region or @config.region ) )
228      _h3 = cryptoAdapter.hmacSha256( _h2, "s3" )
229      _key = cryptoAdapter.hmacSha256( _h3, "aws4_request" )
230  
231      return cryptoAdapter.hmacSha256( _key, policyB64 )
232  
233  
234  

_acl

AwsS3Form._acl( [acl] )

validate the given acl or get the default

Params
[acl=`config.acl`] String the S3 acl
Returns
String A valid acl
API
private
245  
246    _acl: ( acl = @config.acl )=>
247      if acl not in @validation.acl
248        return @_handleError( null, "EINVALIDACL", val: acl )
249      return acl
250    
251  
252  

_redirectUrl

AwsS3Form._redirectUrl( tmpl, data )

Get the default redirect template or process the given sting as lodash template or call teh given function

Params
tmpl String Function A lodash template or function to generate the redirect url. If null general redirectUrlTemplate will be used.
data Object The data object for template or function args. Usual example: { "filename": "the-filename-from-create-or-policy.jpg" }
Returns
String A redirect url
API
private
264  
265    _redirectUrl: ( tmpl = @config.redirectUrlTemplate, data = {} )=>
266      if not tmpl?
267        return @_handleError( null, "ENOREDIR" )
268      
269      if _.isString( tmpl )
270        return _.template( tmpl )( data )
271      else if _.isFunction( tmpl )
272        return tmpl( data )
273      else
274        return @_handleError( null, "EINVALIDREDIR" )
275  
276  
277  

_successActionStatus

AwsS3Form._successActionStatus( status )

Gets the HTTP status code that AWS will return if a redirectUrlTemplate is not defined.

Params
status Number The status code that should be set on successful upload
Returns
Number A redirect url
API
private
288  
289    _successActionStatus: ( status = @config.successActionStatus )=>
290      if status not in @validation.successActionStatus
291        return @_handleError( null, "EINVALIDSTATUS", val: status )
292      return status.toString()
293  
294  
295  

_calcDate

AwsS3Form._calcDate( addSec [, date] )

Calculate and validate a date

Params
addSec Number Date A date to convert or a number in seconds to add to the date out of the date arg.
[date=`new Date Date()] A base date for adding if the first argumentaddSec` is a number.
Returns
String A date ISO String
API
private
307  
308    _calcDate: ( addSec, date = new Date() )=>
309      _msAdd = 0
310      _now = Date.now()
311      
312      if _.isNumber( addSec )
313        _msAdd = addSec * 1000
314        if _.isDate( date )
315          _ts = date.valueOf()
316        else if _.isNumber( date )
317          _ts = date
318        else
319          return @_handleError( null, "ENOTDATE", val: date )
320      else if _.isDate( addSec )
321        _ts = addSec.valueOf()
322      else
323        return @_handleError( null, "ENOTDATE", val: addSec )
324  
325  
326  

use a 10s time space to the past to check the date

325  
326      if ( _now - 10000 ) > _ts
327        return @_handleError( null, "EOLDDATE", val: _now )
328      
329      return ( new Date( _ts + _msAdd ) ).toISOString()
330  
331  
332  

_createCredential

AwsS3Form._createCredential( date )

Generate a AWS Signature Version 4 conform credential string

Params
date Date the credential date
Returns
String a valid AWS Signature Version 4 credential string
API
private
343  
344    _createCredential: ( date )=>
345      shortDate = @_shortDate( date, true )
346      return "#{@config.accessKeyId}/#{shortDate}/#{@config.region}/s3/aws4_request"
347  
348  
349  

_shortDate

AwsS3Form._shortDate( [date] [, onlyDate] )

Create a AWS valid date string

Params
[date=`new Date Date()`] The date to process
[onlyDate=false] Boolean Return only the date and cut the time
Returns
String a AWS valid date string
API
private
361  
362    _shortDate: ( date = new Date(), onlyDate = false )->
363      _sfull = date.toISOString().replace( /\.[0-9]{1,3}Z/g, "Z" ).replace( /[\.:-]/g, "" )
364      if onlyDate
365        return _sfull.substr( 0, 8 )
366      return _sfull
367  
368  
369  

_setCryptoModule

AwsS3Form._setCryptoModule( cryptoModule )

Define the crypto module type

Params
cryptoModule String The secret module to require. ( Enum: crypto, crypto-js )
API
private
378  
379    _setCryptoModule: ( cryptoModule = @config.cryptoModule )=>
380      if cryptoModule not in @validation.cryptoModules
381        @_handleError( null, "EINVALIDCRYPTOMODULE", val: cryptoModule )
382        return
383              
384      cryptoAdapter.setModule( cryptoModule )
385      return
386  
387  
388  

_obj2b64

AwsS3Form._obj2b64( obj )

Srtingify a object and return it base64 encoded. Used to convert the policy result to the base64 string required by the .sign() method.

Params
obj Object A object to stringify
Returns
String Base64 encoded JSON
API
private
399  
400    _obj2b64: ( obj )->
401      return new Buffer( JSON.stringify( obj ) ).toString('base64')
402  
403    ERRORS: =>
404      "EINVALIDCRYPTOMODULE": [ 500, "The given cryptoModule `<%= val %>` is not valid. Only `#{@validation.cryptoModules.join('`, `')}` is allowed." ]
405      "ENOTDATE": [ 500, "Invalid date `<%= val %>`. Please use a valid date object or number as timestamp" ]
406      "EOLDDATE": [ 500, "Date `<%= val %>` to old. Only dates in the future are allowed" ]
407      "EINVALIDACL": [ 500, "The given acl `<%= val %>` is not valid. Only `#{@validation.acl.join('`, `')}` is allowed." ]
408      "EINVALIDSTATUS": [ 500, "The given successActionStatus `<%= val %>` is not valid. Only `#{@validation.successActionStatus.join('`, `')}` is allowed." ]
409      "ENOREDIR": [ 500, "You have to define a `redirectUrlTemplate` as config or `.create()` option." ]
410      "EINVALIDREDIR": [ 500, "Only a string or function is valid as redirect url." ]
411  
412  
413  
414  

export this class

413  
414  module.exports = AwsS3Form
415