gl-store-s3.js/node_modules/aws-sdk/lib/services/s3.js

1086 lines
38 KiB
JavaScript

var AWS = require('../core');
var v4Credentials = require('../signers/v4_credentials');
// Pull in managed upload extension
require('../s3/managed_upload');
/**
* @api private
*/
var operationsWith200StatusCodeError = {
'completeMultipartUpload': true,
'copyObject': true,
'uploadPartCopy': true
};
/**
* @api private
*/
var regionRedirectErrorCodes = [
'AuthorizationHeaderMalformed', // non-head operations on virtual-hosted global bucket endpoints
'BadRequest', // head operations on virtual-hosted global bucket endpoints
'PermanentRedirect', // non-head operations on path-style or regional endpoints
301 // head operations on path-style or regional endpoints
];
AWS.util.update(AWS.S3.prototype, {
/**
* @api private
*/
getSignatureVersion: function getSignatureVersion(request) {
var defaultApiVersion = this.api.signatureVersion;
var userDefinedVersion = this._originalConfig ? this._originalConfig.signatureVersion : null;
var regionDefinedVersion = this.config.signatureVersion;
var isPresigned = request ? request.isPresigned() : false;
/*
1) User defined version specified:
a) always return user defined version
2) No user defined version specified:
a) default to lowest version the region supports
b) If using presigned urls, default to lowest version the region supports
*/
if (userDefinedVersion) {
userDefinedVersion = userDefinedVersion === 'v2' ? 's3' : userDefinedVersion;
return userDefinedVersion;
}
if (isPresigned !== true) {
defaultApiVersion = 'v4';
} else if (regionDefinedVersion) {
defaultApiVersion = regionDefinedVersion;
}
return defaultApiVersion;
},
/**
* @api private
*/
getSignerClass: function getSignerClass(request) {
var signatureVersion = this.getSignatureVersion(request);
return AWS.Signers.RequestSigner.getVersion(signatureVersion);
},
/**
* @api private
*/
validateService: function validateService() {
var msg;
var messages = [];
// default to us-east-1 when no region is provided
if (!this.config.region) this.config.region = 'us-east-1';
if (!this.config.endpoint && this.config.s3BucketEndpoint) {
messages.push('An endpoint must be provided when configuring ' +
'`s3BucketEndpoint` to true.');
}
if (messages.length === 1) {
msg = messages[0];
} else if (messages.length > 1) {
msg = 'Multiple configuration errors:\n' + messages.join('\n');
}
if (msg) {
throw AWS.util.error(new Error(),
{name: 'InvalidEndpoint', message: msg});
}
},
/**
* @api private
*/
shouldDisableBodySigning: function shouldDisableBodySigning(request) {
var signerClass = this.getSignerClass();
if (this.config.s3DisableBodySigning === true && signerClass === AWS.Signers.V4
&& request.httpRequest.endpoint.protocol === 'https:') {
return true;
}
return false;
},
/**
* @api private
*/
setupRequestListeners: function setupRequestListeners(request) {
var prependListener = true;
request.addListener('validate', this.validateScheme);
request.addListener('validate', this.validateBucketEndpoint);
request.addListener('validate', this.correctBucketRegionFromCache);
request.addListener('validate', this.validateBucketName, prependListener);
request.addListener('build', this.addContentType);
request.addListener('build', this.populateURI);
request.addListener('build', this.computeContentMd5);
request.addListener('build', this.computeSseCustomerKeyMd5);
request.addListener('afterBuild', this.addExpect100Continue);
request.removeListener('validate',
AWS.EventListeners.Core.VALIDATE_REGION);
request.addListener('extractError', this.extractError);
request.onAsync('extractError', this.requestBucketRegion);
request.addListener('extractData', this.extractData);
request.addListener('extractData', AWS.util.hoistPayloadMember);
request.addListener('beforePresign', this.prepareSignedUrl);
if (AWS.util.isBrowser()) {
request.onAsync('retry', this.reqRegionForNetworkingError);
}
if (this.shouldDisableBodySigning(request)) {
request.removeListener('afterBuild', AWS.EventListeners.Core.COMPUTE_SHA256);
request.addListener('afterBuild', this.disableBodySigning);
}
},
/**
* @api private
*/
validateScheme: function(req) {
var params = req.params,
scheme = req.httpRequest.endpoint.protocol,
sensitive = params.SSECustomerKey || params.CopySourceSSECustomerKey;
if (sensitive && scheme !== 'https:') {
var msg = 'Cannot send SSE keys over HTTP. Set \'sslEnabled\'' +
'to \'true\' in your configuration';
throw AWS.util.error(new Error(),
{ code: 'ConfigError', message: msg });
}
},
/**
* @api private
*/
validateBucketEndpoint: function(req) {
if (!req.params.Bucket && req.service.config.s3BucketEndpoint) {
var msg = 'Cannot send requests to root API with `s3BucketEndpoint` set.';
throw AWS.util.error(new Error(),
{ code: 'ConfigError', message: msg });
}
},
/**
* @api private
*/
validateBucketName: function validateBucketName(req) {
var service = req.service;
var signatureVersion = service.getSignatureVersion(req);
var bucket = req.params && req.params.Bucket;
var key = req.params && req.params.Key;
var slashIndex = bucket && bucket.indexOf('/');
if (bucket && slashIndex >= 0) {
if (typeof key === 'string' && slashIndex > 0) {
req.params = AWS.util.copy(req.params);
// Need to include trailing slash to match sigv2 behavior
var prefix = bucket.substr(slashIndex + 1) || '';
req.params.Key = prefix + '/' + key;
req.params.Bucket = bucket.substr(0, slashIndex);
} else if (signatureVersion === 'v4') {
var msg = 'Bucket names cannot contain forward slashes. Bucket: ' + bucket;
throw AWS.util.error(new Error(),
{ code: 'InvalidBucket', message: msg });
}
}
},
/**
* @api private
*/
isValidAccelerateOperation: function isValidAccelerateOperation(operation) {
var invalidOperations = [
'createBucket',
'deleteBucket',
'listBuckets'
];
return invalidOperations.indexOf(operation) === -1;
},
/**
* S3 prefers dns-compatible bucket names to be moved from the uri path
* to the hostname as a sub-domain. This is not possible, even for dns-compat
* buckets when using SSL and the bucket name contains a dot ('.'). The
* ssl wildcard certificate is only 1-level deep.
*
* @api private
*/
populateURI: function populateURI(req) {
var httpRequest = req.httpRequest;
var b = req.params.Bucket;
var service = req.service;
var endpoint = httpRequest.endpoint;
if (b) {
if (!service.pathStyleBucketName(b)) {
if (service.config.useAccelerateEndpoint && service.isValidAccelerateOperation(req.operation)) {
if (service.config.useDualstack) {
endpoint.hostname = b + '.s3-accelerate.dualstack.amazonaws.com';
} else {
endpoint.hostname = b + '.s3-accelerate.amazonaws.com';
}
} else if (!service.config.s3BucketEndpoint) {
endpoint.hostname =
b + '.' + endpoint.hostname;
}
var port = endpoint.port;
if (port !== 80 && port !== 443) {
endpoint.host = endpoint.hostname + ':' +
endpoint.port;
} else {
endpoint.host = endpoint.hostname;
}
httpRequest.virtualHostedBucket = b; // needed for signing the request
service.removeVirtualHostedBucketFromPath(req);
}
}
},
/**
* Takes the bucket name out of the path if bucket is virtual-hosted
*
* @api private
*/
removeVirtualHostedBucketFromPath: function removeVirtualHostedBucketFromPath(req) {
var httpRequest = req.httpRequest;
var bucket = httpRequest.virtualHostedBucket;
if (bucket && httpRequest.path) {
if (req.params && req.params.Key) {
var encodedS3Key = '/' + AWS.util.uriEscapePath(req.params.Key);
if (httpRequest.path.indexOf(encodedS3Key) === 0 && (httpRequest.path.length === encodedS3Key.length || httpRequest.path[encodedS3Key.length] === '?')) {
//path only contains key or path contains only key and querystring
return;
}
}
httpRequest.path = httpRequest.path.replace(new RegExp('/' + bucket), '');
if (httpRequest.path[0] !== '/') {
httpRequest.path = '/' + httpRequest.path;
}
}
},
/**
* Adds Expect: 100-continue header if payload is greater-or-equal 1MB
* @api private
*/
addExpect100Continue: function addExpect100Continue(req) {
var len = req.httpRequest.headers['Content-Length'];
if (AWS.util.isNode() && (len >= 1024 * 1024 || req.params.Body instanceof AWS.util.stream.Stream)) {
req.httpRequest.headers['Expect'] = '100-continue';
}
},
/**
* Adds a default content type if none is supplied.
*
* @api private
*/
addContentType: function addContentType(req) {
var httpRequest = req.httpRequest;
if (httpRequest.method === 'GET' || httpRequest.method === 'HEAD') {
// Content-Type is not set in GET/HEAD requests
delete httpRequest.headers['Content-Type'];
return;
}
if (!httpRequest.headers['Content-Type']) { // always have a Content-Type
httpRequest.headers['Content-Type'] = 'application/octet-stream';
}
var contentType = httpRequest.headers['Content-Type'];
if (AWS.util.isBrowser()) {
if (typeof httpRequest.body === 'string' && !contentType.match(/;\s*charset=/)) {
var charset = '; charset=UTF-8';
httpRequest.headers['Content-Type'] += charset;
} else {
var replaceFn = function(_, prefix, charsetName) {
return prefix + charsetName.toUpperCase();
};
httpRequest.headers['Content-Type'] =
contentType.replace(/(;\s*charset=)(.+)$/, replaceFn);
}
}
},
/**
* @api private
*/
computableChecksumOperations: {
putBucketCors: true,
putBucketLifecycle: true,
putBucketLifecycleConfiguration: true,
putBucketTagging: true,
deleteObjects: true,
putBucketReplication: true,
putObjectLegalHold: true,
putObjectRetention: true,
putObjectLockConfiguration: true
},
/**
* Checks whether checksums should be computed for the request.
* If the request requires checksums to be computed, this will always
* return true, otherwise it depends on whether {AWS.Config.computeChecksums}
* is set.
*
* @param req [AWS.Request] the request to check against
* @return [Boolean] whether to compute checksums for a request.
* @api private
*/
willComputeChecksums: function willComputeChecksums(req) {
if (this.computableChecksumOperations[req.operation]) return true;
if (!this.config.computeChecksums) return false;
// TODO: compute checksums for Stream objects
if (!AWS.util.Buffer.isBuffer(req.httpRequest.body) &&
typeof req.httpRequest.body !== 'string') {
return false;
}
var rules = req.service.api.operations[req.operation].input.members;
// Sha256 signing disabled, and not a presigned url
if (req.service.shouldDisableBodySigning(req) && !Object.prototype.hasOwnProperty.call(req.httpRequest.headers, 'presigned-expires')) {
if (rules.ContentMD5 && !req.params.ContentMD5) {
return true;
}
}
// V4 signer uses SHA256 signatures so only compute MD5 if it is required
if (req.service.getSignerClass(req) === AWS.Signers.V4) {
if (rules.ContentMD5 && !rules.ContentMD5.required) return false;
}
if (rules.ContentMD5 && !req.params.ContentMD5) return true;
},
/**
* A listener that computes the Content-MD5 and sets it in the header.
* @see AWS.S3.willComputeChecksums
* @api private
*/
computeContentMd5: function computeContentMd5(req) {
if (req.service.willComputeChecksums(req)) {
var md5 = AWS.util.crypto.md5(req.httpRequest.body, 'base64');
req.httpRequest.headers['Content-MD5'] = md5;
}
},
/**
* @api private
*/
computeSseCustomerKeyMd5: function computeSseCustomerKeyMd5(req) {
var keys = {
SSECustomerKey: 'x-amz-server-side-encryption-customer-key-MD5',
CopySourceSSECustomerKey: 'x-amz-copy-source-server-side-encryption-customer-key-MD5'
};
AWS.util.each(keys, function(key, header) {
if (req.params[key]) {
var value = AWS.util.crypto.md5(req.params[key], 'base64');
req.httpRequest.headers[header] = value;
}
});
},
/**
* Returns true if the bucket name should be left in the URI path for
* a request to S3. This function takes into account the current
* endpoint protocol (e.g. http or https).
*
* @api private
*/
pathStyleBucketName: function pathStyleBucketName(bucketName) {
// user can force path style requests via the configuration
if (this.config.s3ForcePathStyle) return true;
if (this.config.s3BucketEndpoint) return false;
if (this.dnsCompatibleBucketName(bucketName)) {
return (this.config.sslEnabled && bucketName.match(/\./)) ? true : false;
} else {
return true; // not dns compatible names must always use path style
}
},
/**
* Returns true if the bucket name is DNS compatible. Buckets created
* outside of the classic region MUST be DNS compatible.
*
* @api private
*/
dnsCompatibleBucketName: function dnsCompatibleBucketName(bucketName) {
var b = bucketName;
var domain = new RegExp(/^[a-z0-9][a-z0-9\.\-]{1,61}[a-z0-9]$/);
var ipAddress = new RegExp(/(\d+\.){3}\d+/);
var dots = new RegExp(/\.\./);
return (b.match(domain) && !b.match(ipAddress) && !b.match(dots)) ? true : false;
},
/**
* @return [Boolean] whether response contains an error
* @api private
*/
successfulResponse: function successfulResponse(resp) {
var req = resp.request;
var httpResponse = resp.httpResponse;
if (operationsWith200StatusCodeError[req.operation] &&
httpResponse.body.toString().match('<Error>')) {
return false;
} else {
return httpResponse.statusCode < 300;
}
},
/**
* @return [Boolean] whether the error can be retried
* @api private
*/
retryableError: function retryableError(error, request) {
if (operationsWith200StatusCodeError[request.operation] &&
error.statusCode === 200) {
return true;
} else if (request._requestRegionForBucket &&
request.service.bucketRegionCache[request._requestRegionForBucket]) {
return false;
} else if (error && error.code === 'RequestTimeout') {
return true;
} else if (error &&
regionRedirectErrorCodes.indexOf(error.code) != -1 &&
error.region && error.region != request.httpRequest.region) {
request.httpRequest.region = error.region;
if (error.statusCode === 301) {
request.service.updateReqBucketRegion(request);
}
return true;
} else {
var _super = AWS.Service.prototype.retryableError;
return _super.call(this, error, request);
}
},
/**
* Updates httpRequest with region. If region is not provided, then
* the httpRequest will be updated based on httpRequest.region
*
* @api private
*/
updateReqBucketRegion: function updateReqBucketRegion(request, region) {
var httpRequest = request.httpRequest;
if (typeof region === 'string' && region.length) {
httpRequest.region = region;
}
if (!httpRequest.endpoint.host.match(/s3(?!-accelerate).*\.amazonaws\.com$/)) {
return;
}
var service = request.service;
var s3Config = service.config;
var s3BucketEndpoint = s3Config.s3BucketEndpoint;
if (s3BucketEndpoint) {
delete s3Config.s3BucketEndpoint;
}
var newConfig = AWS.util.copy(s3Config);
delete newConfig.endpoint;
newConfig.region = httpRequest.region;
httpRequest.endpoint = (new AWS.S3(newConfig)).endpoint;
service.populateURI(request);
s3Config.s3BucketEndpoint = s3BucketEndpoint;
httpRequest.headers.Host = httpRequest.endpoint.host;
if (request._asm.currentState === 'validate') {
request.removeListener('build', service.populateURI);
request.addListener('build', service.removeVirtualHostedBucketFromPath);
}
},
/**
* Provides a specialized parser for getBucketLocation -- all other
* operations are parsed by the super class.
*
* @api private
*/
extractData: function extractData(resp) {
var req = resp.request;
if (req.operation === 'getBucketLocation') {
var match = resp.httpResponse.body.toString().match(/>(.+)<\/Location/);
delete resp.data['_'];
if (match) {
resp.data.LocationConstraint = match[1];
} else {
resp.data.LocationConstraint = '';
}
}
var bucket = req.params.Bucket || null;
if (req.operation === 'deleteBucket' && typeof bucket === 'string' && !resp.error) {
req.service.clearBucketRegionCache(bucket);
} else {
var headers = resp.httpResponse.headers || {};
var region = headers['x-amz-bucket-region'] || null;
if (!region && req.operation === 'createBucket' && !resp.error) {
var createBucketConfiguration = req.params.CreateBucketConfiguration;
if (!createBucketConfiguration) {
region = 'us-east-1';
} else if (createBucketConfiguration.LocationConstraint === 'EU') {
region = 'eu-west-1';
} else {
region = createBucketConfiguration.LocationConstraint;
}
}
if (region) {
if (bucket && region !== req.service.bucketRegionCache[bucket]) {
req.service.bucketRegionCache[bucket] = region;
}
}
}
req.service.extractRequestIds(resp);
},
/**
* Extracts an error object from the http response.
*
* @api private
*/
extractError: function extractError(resp) {
var codes = {
304: 'NotModified',
403: 'Forbidden',
400: 'BadRequest',
404: 'NotFound'
};
var req = resp.request;
var code = resp.httpResponse.statusCode;
var body = resp.httpResponse.body || '';
var headers = resp.httpResponse.headers || {};
var region = headers['x-amz-bucket-region'] || null;
var bucket = req.params.Bucket || null;
var bucketRegionCache = req.service.bucketRegionCache;
if (region && bucket && region !== bucketRegionCache[bucket]) {
bucketRegionCache[bucket] = region;
}
var cachedRegion;
if (codes[code] && body.length === 0) {
if (bucket && !region) {
cachedRegion = bucketRegionCache[bucket] || null;
if (cachedRegion !== req.httpRequest.region) {
region = cachedRegion;
}
}
resp.error = AWS.util.error(new Error(), {
code: codes[code],
message: null,
region: region
});
} else {
var data = new AWS.XML.Parser().parse(body.toString());
if (data.Region && !region) {
region = data.Region;
if (bucket && region !== bucketRegionCache[bucket]) {
bucketRegionCache[bucket] = region;
}
} else if (bucket && !region && !data.Region) {
cachedRegion = bucketRegionCache[bucket] || null;
if (cachedRegion !== req.httpRequest.region) {
region = cachedRegion;
}
}
resp.error = AWS.util.error(new Error(), {
code: data.Code || code,
message: data.Message || null,
region: region
});
}
req.service.extractRequestIds(resp);
},
/**
* If region was not obtained synchronously, then send async request
* to get bucket region for errors resulting from wrong region.
*
* @api private
*/
requestBucketRegion: function requestBucketRegion(resp, done) {
var error = resp.error;
var req = resp.request;
var bucket = req.params.Bucket || null;
if (!error || !bucket || error.region || req.operation === 'listObjects' ||
(AWS.util.isNode() && req.operation === 'headBucket') ||
(error.statusCode === 400 && req.operation !== 'headObject') ||
regionRedirectErrorCodes.indexOf(error.code) === -1) {
return done();
}
var reqOperation = AWS.util.isNode() ? 'headBucket' : 'listObjects';
var reqParams = {Bucket: bucket};
if (reqOperation === 'listObjects') reqParams.MaxKeys = 0;
var regionReq = req.service[reqOperation](reqParams);
regionReq._requestRegionForBucket = bucket;
regionReq.send(function() {
var region = req.service.bucketRegionCache[bucket] || null;
error.region = region;
done();
});
},
/**
* For browser only. If NetworkingError received, will attempt to obtain
* the bucket region.
*
* @api private
*/
reqRegionForNetworkingError: function reqRegionForNetworkingError(resp, done) {
if (!AWS.util.isBrowser()) {
return done();
}
var error = resp.error;
var request = resp.request;
var bucket = request.params.Bucket;
if (!error || error.code !== 'NetworkingError' || !bucket ||
request.httpRequest.region === 'us-east-1') {
return done();
}
var service = request.service;
var bucketRegionCache = service.bucketRegionCache;
var cachedRegion = bucketRegionCache[bucket] || null;
if (cachedRegion && cachedRegion !== request.httpRequest.region) {
service.updateReqBucketRegion(request, cachedRegion);
done();
} else if (!service.dnsCompatibleBucketName(bucket)) {
service.updateReqBucketRegion(request, 'us-east-1');
if (bucketRegionCache[bucket] !== 'us-east-1') {
bucketRegionCache[bucket] = 'us-east-1';
}
done();
} else if (request.httpRequest.virtualHostedBucket) {
var getRegionReq = service.listObjects({Bucket: bucket, MaxKeys: 0});
service.updateReqBucketRegion(getRegionReq, 'us-east-1');
getRegionReq._requestRegionForBucket = bucket;
getRegionReq.send(function() {
var region = service.bucketRegionCache[bucket] || null;
if (region && region !== request.httpRequest.region) {
service.updateReqBucketRegion(request, region);
}
done();
});
} else {
// DNS-compatible path-style
// (s3ForcePathStyle or bucket name with dot over https)
// Cannot obtain region information for this case
done();
}
},
/**
* Cache for bucket region.
*
* @api private
*/
bucketRegionCache: {},
/**
* Clears bucket region cache.
*
* @api private
*/
clearBucketRegionCache: function(buckets) {
var bucketRegionCache = this.bucketRegionCache;
if (!buckets) {
buckets = Object.keys(bucketRegionCache);
} else if (typeof buckets === 'string') {
buckets = [buckets];
}
for (var i = 0; i < buckets.length; i++) {
delete bucketRegionCache[buckets[i]];
}
return bucketRegionCache;
},
/**
* Corrects request region if bucket's cached region is different
*
* @api private
*/
correctBucketRegionFromCache: function correctBucketRegionFromCache(req) {
var bucket = req.params.Bucket || null;
if (bucket) {
var service = req.service;
var requestRegion = req.httpRequest.region;
var cachedRegion = service.bucketRegionCache[bucket];
if (cachedRegion && cachedRegion !== requestRegion) {
service.updateReqBucketRegion(req, cachedRegion);
}
}
},
/**
* Extracts S3 specific request ids from the http response.
*
* @api private
*/
extractRequestIds: function extractRequestIds(resp) {
var extendedRequestId = resp.httpResponse.headers ? resp.httpResponse.headers['x-amz-id-2'] : null;
var cfId = resp.httpResponse.headers ? resp.httpResponse.headers['x-amz-cf-id'] : null;
resp.extendedRequestId = extendedRequestId;
resp.cfId = cfId;
if (resp.error) {
resp.error.requestId = resp.requestId || null;
resp.error.extendedRequestId = extendedRequestId;
resp.error.cfId = cfId;
}
},
/**
* Get a pre-signed URL for a given operation name.
*
* @note You must ensure that you have static or previously resolved
* credentials if you call this method synchronously (with no callback),
* otherwise it may not properly sign the request. If you cannot guarantee
* this (you are using an asynchronous credential provider, i.e., EC2
* IAM roles), you should always call this method with an asynchronous
* callback.
* @note Not all operation parameters are supported when using pre-signed
* URLs. Certain parameters, such as `SSECustomerKey`, `ACL`, `Expires`,
* `ContentLength`, or `Tagging` must be provided as headers when sending a
* request. If you are using pre-signed URLs to upload from a browser and
* need to use these fields, see {createPresignedPost}.
* @note The default signer allows altering the request by adding corresponding
* headers to set some parameters (e.g. Range) and these added parameters
* won't be signed. You must use signatureVersion v4 to to include these
* parameters in the signed portion of the URL and enforce exact matching
* between headers and signed params in the URL.
* @note This operation cannot be used with a promise. See note above regarding
* asynchronous credentials and use with a callback.
* @param operation [String] the name of the operation to call
* @param params [map] parameters to pass to the operation. See the given
* operation for the expected operation parameters. In addition, you can
* also pass the "Expires" parameter to inform S3 how long the URL should
* work for.
* @option params Expires [Integer] (900) the number of seconds to expire
* the pre-signed URL operation in. Defaults to 15 minutes.
* @param callback [Function] if a callback is provided, this function will
* pass the URL as the second parameter (after the error parameter) to
* the callback function.
* @return [String] if called synchronously (with no callback), returns the
* signed URL.
* @return [null] nothing is returned if a callback is provided.
* @example Pre-signing a getObject operation (synchronously)
* var params = {Bucket: 'bucket', Key: 'key'};
* var url = s3.getSignedUrl('getObject', params);
* console.log('The URL is', url);
* @example Pre-signing a putObject (asynchronously)
* var params = {Bucket: 'bucket', Key: 'key'};
* s3.getSignedUrl('putObject', params, function (err, url) {
* console.log('The URL is', url);
* });
* @example Pre-signing a putObject operation with a specific payload
* var params = {Bucket: 'bucket', Key: 'key', Body: 'body'};
* var url = s3.getSignedUrl('putObject', params);
* console.log('The URL is', url);
* @example Passing in a 1-minute expiry time for a pre-signed URL
* var params = {Bucket: 'bucket', Key: 'key', Expires: 60};
* var url = s3.getSignedUrl('getObject', params);
* console.log('The URL is', url); // expires in 60 seconds
*/
getSignedUrl: function getSignedUrl(operation, params, callback) {
params = AWS.util.copy(params || {});
var expires = params.Expires || 900;
delete params.Expires; // we can't validate this
var request = this.makeRequest(operation, params);
if (callback) {
AWS.util.defer(function() {
request.presign(expires, callback);
});
} else {
return request.presign(expires, callback);
}
},
/**
* Get a pre-signed POST policy to support uploading to S3 directly from an
* HTML form.
*
* @param params [map]
* @option params Bucket [String] The bucket to which the post should be
* uploaded
* @option params Expires [Integer] (3600) The number of seconds for which
* the presigned policy should be valid.
* @option params Conditions [Array] An array of conditions that must be met
* for the presigned policy to allow the
* upload. This can include required tags,
* the accepted range for content lengths,
* etc.
* @see http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html
* @option params Fields [map] Fields to include in the form. All
* values passed in as fields will be
* signed as exact match conditions.
* @param callback [Function]
*
* @note All fields passed in when creating presigned post data will be signed
* as exact match conditions. Any fields that will be interpolated by S3
* must be added to the fields hash after signing, and an appropriate
* condition for such fields must be explicitly added to the Conditions
* array passed to this function before signing.
*
* @example Presiging post data with a known key
* var params = {
* Bucket: 'bucket',
* Fields: {
* key: 'key'
* }
* };
* s3.createPresignedPost(params, function(err, data) {
* if (err) {
* console.error('Presigning post data encountered an error', err);
* } else {
* console.log('The post data is', data);
* }
* });
*
* @example Presigning post data with an interpolated key
* var params = {
* Bucket: 'bucket',
* Conditions: [
* ['starts-with', '$key', 'path/to/uploads/']
* ]
* };
* s3.createPresignedPost(params, function(err, data) {
* if (err) {
* console.error('Presigning post data encountered an error', err);
* } else {
* data.Fields.key = 'path/to/uploads/${filename}';
* console.log('The post data is', data);
* }
* });
*
* @note You must ensure that you have static or previously resolved
* credentials if you call this method synchronously (with no callback),
* otherwise it may not properly sign the request. If you cannot guarantee
* this (you are using an asynchronous credential provider, i.e., EC2
* IAM roles), you should always call this method with an asynchronous
* callback.
*
* @return [map] If called synchronously (with no callback), returns a hash
* with the url to set as the form action and a hash of fields
* to include in the form.
* @return [null] Nothing is returned if a callback is provided.
*
* @callback callback function (err, data)
* @param err [Error] the error object returned from the policy signer
* @param data [map] The data necessary to construct an HTML form
* @param data.url [String] The URL to use as the action of the form
* @param data.fields [map] A hash of fields that must be included in the
* form for the upload to succeed. This hash will
* include the signed POST policy, your access key
* ID and security token (if present), etc. These
* may be safely included as input elements of type
* 'hidden.'
*/
createPresignedPost: function createPresignedPost(params, callback) {
if (typeof params === 'function' && callback === undefined) {
callback = params;
params = null;
}
params = AWS.util.copy(params || {});
var boundParams = this.config.params || {};
var bucket = params.Bucket || boundParams.Bucket,
self = this,
config = this.config,
endpoint = AWS.util.copy(this.endpoint);
if (!config.s3BucketEndpoint) {
endpoint.pathname = '/' + bucket;
}
function finalizePost() {
return {
url: AWS.util.urlFormat(endpoint),
fields: self.preparePostFields(
config.credentials,
config.region,
bucket,
params.Fields,
params.Conditions,
params.Expires
)
};
}
if (callback) {
config.getCredentials(function (err) {
if (err) {
callback(err);
}
callback(null, finalizePost());
});
} else {
return finalizePost();
}
},
/**
* @api private
*/
preparePostFields: function preparePostFields(
credentials,
region,
bucket,
fields,
conditions,
expiresInSeconds
) {
var now = this.getSkewCorrectedDate();
if (!credentials || !region || !bucket) {
throw new Error('Unable to create a POST object policy without a bucket,'
+ ' region, and credentials');
}
fields = AWS.util.copy(fields || {});
conditions = (conditions || []).slice(0);
expiresInSeconds = expiresInSeconds || 3600;
var signingDate = AWS.util.date.iso8601(now).replace(/[:\-]|\.\d{3}/g, '');
var shortDate = signingDate.substr(0, 8);
var scope = v4Credentials.createScope(shortDate, region, 's3');
var credential = credentials.accessKeyId + '/' + scope;
fields['bucket'] = bucket;
fields['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256';
fields['X-Amz-Credential'] = credential;
fields['X-Amz-Date'] = signingDate;
if (credentials.sessionToken) {
fields['X-Amz-Security-Token'] = credentials.sessionToken;
}
for (var field in fields) {
if (fields.hasOwnProperty(field)) {
var condition = {};
condition[field] = fields[field];
conditions.push(condition);
}
}
fields.Policy = this.preparePostPolicy(
new Date(now.valueOf() + expiresInSeconds * 1000),
conditions
);
fields['X-Amz-Signature'] = AWS.util.crypto.hmac(
v4Credentials.getSigningKey(credentials, shortDate, region, 's3', true),
fields.Policy,
'hex'
);
return fields;
},
/**
* @api private
*/
preparePostPolicy: function preparePostPolicy(expiration, conditions) {
return AWS.util.base64.encode(JSON.stringify({
expiration: AWS.util.date.iso8601(expiration),
conditions: conditions
}));
},
/**
* @api private
*/
prepareSignedUrl: function prepareSignedUrl(request) {
request.addListener('validate', request.service.noPresignedContentLength);
request.removeListener('build', request.service.addContentType);
if (!request.params.Body) {
// no Content-MD5/SHA-256 if body is not provided
request.removeListener('build', request.service.computeContentMd5);
} else {
request.addListener('afterBuild', AWS.EventListeners.Core.COMPUTE_SHA256);
}
},
/**
* @api private
* @param request
*/
disableBodySigning: function disableBodySigning(request) {
var headers = request.httpRequest.headers;
// Add the header to anything that isn't a presigned url, unless that presigned url had a body defined
if (!Object.prototype.hasOwnProperty.call(headers, 'presigned-expires')) {
headers['X-Amz-Content-Sha256'] = 'UNSIGNED-PAYLOAD';
}
},
/**
* @api private
*/
noPresignedContentLength: function noPresignedContentLength(request) {
if (request.params.ContentLength !== undefined) {
throw AWS.util.error(new Error(), {code: 'UnexpectedParameter',
message: 'ContentLength is not supported in pre-signed URLs.'});
}
},
createBucket: function createBucket(params, callback) {
// When creating a bucket *outside* the classic region, the location
// constraint must be set for the bucket and it must match the endpoint.
// This chunk of code will set the location constraint param based
// on the region (when possible), but it will not override a passed-in
// location constraint.
if (typeof params === 'function' || !params) {
callback = callback || params;
params = {};
}
var hostname = this.endpoint.hostname;
if (hostname !== this.api.globalEndpoint && !params.CreateBucketConfiguration) {
params.CreateBucketConfiguration = { LocationConstraint: this.config.region };
}
return this.makeRequest('createBucket', params, callback);
},
/**
* @see AWS.S3.ManagedUpload
* @overload upload(params = {}, [options], [callback])
* Uploads an arbitrarily sized buffer, blob, or stream, using intelligent
* concurrent handling of parts if the payload is large enough. You can
* configure the concurrent queue size by setting `options`. Note that this
* is the only operation for which the SDK can retry requests with stream
* bodies.
*
* @param (see AWS.S3.putObject)
* @option (see AWS.S3.ManagedUpload.constructor)
* @return [AWS.S3.ManagedUpload] the managed upload object that can call
* `send()` or track progress.
* @example Uploading a stream object
* var params = {Bucket: 'bucket', Key: 'key', Body: stream};
* s3.upload(params, function(err, data) {
* console.log(err, data);
* });
* @example Uploading a stream with concurrency of 1 and partSize of 10mb
* var params = {Bucket: 'bucket', Key: 'key', Body: stream};
* var options = {partSize: 10 * 1024 * 1024, queueSize: 1};
* s3.upload(params, options, function(err, data) {
* console.log(err, data);
* });
* @callback callback function(err, data)
* @param err [Error] an error or null if no error occurred.
* @param data [map] The response data from the successful upload:
* @param data.Location [String] the URL of the uploaded object
* @param data.ETag [String] the ETag of the uploaded object
* @param data.Bucket [String] the bucket to which the object was uploaded
* @param data.Key [String] the key to which the object was uploaded
*/
upload: function upload(params, options, callback) {
if (typeof options === 'function' && callback === undefined) {
callback = options;
options = null;
}
options = options || {};
options = AWS.util.merge(options || {}, {service: this, params: params});
var uploader = new AWS.S3.ManagedUpload(options);
if (typeof callback === 'function') uploader.send(callback);
return uploader;
}
});