s3.js (29035B)
1 var AWS = require('../core'); 2 3 // Pull in managed upload extension 4 require('../s3/managed_upload'); 5 6 /** 7 * @api private 8 */ 9 var operationsWith200StatusCodeError = { 10 'completeMultipartUpload': true, 11 'copyObject': true, 12 'uploadPartCopy': true 13 }; 14 15 /** 16 * @api private 17 */ 18 var regionRedirectErrorCodes = [ 19 'AuthorizationHeaderMalformed', // non-head operations on virtual-hosted global bucket endpoints 20 'BadRequest', // head operations on virtual-hosted global bucket endpoints 21 'PermanentRedirect', // non-head operations on path-style or regional endpoints 22 301 // head operations on path-style or regional endpoints 23 ]; 24 25 AWS.util.update(AWS.S3.prototype, { 26 /** 27 * @api private 28 */ 29 getSignerClass: function getSignerClass(request) { 30 var defaultApiVersion = this.api.signatureVersion; 31 var userDefinedVersion = this._originalConfig ? this._originalConfig.signatureVersion : null; 32 var regionDefinedVersion = this.config.signatureVersion; 33 var isPresigned = request ? request.isPresigned() : false; 34 /* 35 1) User defined version specified: 36 a) always return user defined version 37 2) No user defined version specified: 38 a) default to lowest version the region supports 39 */ 40 if (userDefinedVersion) { 41 userDefinedVersion = userDefinedVersion === 'v2' ? 's3' : userDefinedVersion; 42 return AWS.Signers.RequestSigner.getVersion(userDefinedVersion); 43 } 44 if (regionDefinedVersion) { 45 defaultApiVersion = regionDefinedVersion; 46 } 47 48 return AWS.Signers.RequestSigner.getVersion(defaultApiVersion); 49 }, 50 51 /** 52 * @api private 53 */ 54 validateService: function validateService() { 55 var msg; 56 var messages = []; 57 58 // default to us-east-1 when no region is provided 59 if (!this.config.region) this.config.region = 'us-east-1'; 60 61 if (!this.config.endpoint && this.config.s3BucketEndpoint) { 62 messages.push('An endpoint must be provided when configuring ' + 63 '`s3BucketEndpoint` to true.'); 64 } 65 if (messages.length === 1) { 66 msg = messages[0]; 67 } else if (messages.length > 1) { 68 msg = 'Multiple configuration errors:\n' + messages.join('\n'); 69 } 70 if (msg) { 71 throw AWS.util.error(new Error(), 72 {name: 'InvalidEndpoint', message: msg}); 73 } 74 }, 75 76 /** 77 * @api private 78 */ 79 shouldDisableBodySigning: function shouldDisableBodySigning(request) { 80 var signerClass = this.getSignerClass(); 81 if (this.config.s3DisableBodySigning === true && signerClass === AWS.Signers.V4 82 && request.httpRequest.endpoint.protocol === 'https:') { 83 return true; 84 } 85 return false; 86 }, 87 88 /** 89 * @api private 90 */ 91 setupRequestListeners: function setupRequestListeners(request) { 92 request.addListener('validate', this.validateScheme); 93 request.addListener('validate', this.validateBucketEndpoint); 94 request.addListener('validate', this.correctBucketRegionFromCache); 95 request.addListener('build', this.addContentType); 96 request.addListener('build', this.populateURI); 97 request.addListener('build', this.computeContentMd5); 98 request.addListener('build', this.computeSseCustomerKeyMd5); 99 request.addListener('afterBuild', this.addExpect100Continue); 100 request.removeListener('validate', 101 AWS.EventListeners.Core.VALIDATE_REGION); 102 request.addListener('extractError', this.extractError); 103 request.onAsync('extractError', this.requestBucketRegion); 104 request.addListener('extractData', this.extractData); 105 request.addListener('extractData', AWS.util.hoistPayloadMember); 106 request.addListener('beforePresign', this.prepareSignedUrl); 107 if (AWS.util.isBrowser()) { 108 request.onAsync('retry', this.reqRegionForNetworkingError); 109 } 110 if (this.shouldDisableBodySigning(request)) { 111 request.removeListener('afterBuild', AWS.EventListeners.Core.COMPUTE_SHA256); 112 request.addListener('afterBuild', this.disableBodySigning); 113 } 114 }, 115 116 /** 117 * @api private 118 */ 119 validateScheme: function(req) { 120 var params = req.params, 121 scheme = req.httpRequest.endpoint.protocol, 122 sensitive = params.SSECustomerKey || params.CopySourceSSECustomerKey; 123 if (sensitive && scheme !== 'https:') { 124 var msg = 'Cannot send SSE keys over HTTP. Set \'sslEnabled\'' + 125 'to \'true\' in your configuration'; 126 throw AWS.util.error(new Error(), 127 { code: 'ConfigError', message: msg }); 128 } 129 }, 130 131 /** 132 * @api private 133 */ 134 validateBucketEndpoint: function(req) { 135 if (!req.params.Bucket && req.service.config.s3BucketEndpoint) { 136 var msg = 'Cannot send requests to root API with `s3BucketEndpoint` set.'; 137 throw AWS.util.error(new Error(), 138 { code: 'ConfigError', message: msg }); 139 } 140 }, 141 142 /** 143 * @api private 144 */ 145 isValidAccelerateOperation: function isValidAccelerateOperation(operation) { 146 var invalidOperations = [ 147 'createBucket', 148 'deleteBucket', 149 'listBuckets' 150 ]; 151 return invalidOperations.indexOf(operation) === -1; 152 }, 153 154 155 /** 156 * S3 prefers dns-compatible bucket names to be moved from the uri path 157 * to the hostname as a sub-domain. This is not possible, even for dns-compat 158 * buckets when using SSL and the bucket name contains a dot ('.'). The 159 * ssl wildcard certificate is only 1-level deep. 160 * 161 * @api private 162 */ 163 populateURI: function populateURI(req) { 164 var httpRequest = req.httpRequest; 165 var b = req.params.Bucket; 166 var service = req.service; 167 var endpoint = httpRequest.endpoint; 168 169 if (b) { 170 if (!service.pathStyleBucketName(b)) { 171 if (service.config.useAccelerateEndpoint && service.isValidAccelerateOperation(req.operation)) { 172 if (service.config.useDualstack) { 173 endpoint.hostname = b + '.s3-accelerate.dualstack.amazonaws.com'; 174 } else { 175 endpoint.hostname = b + '.s3-accelerate.amazonaws.com'; 176 } 177 } else if (!service.config.s3BucketEndpoint) { 178 endpoint.hostname = 179 b + '.' + endpoint.hostname; 180 } 181 182 var port = endpoint.port; 183 if (port !== 80 && port !== 443) { 184 endpoint.host = endpoint.hostname + ':' + 185 endpoint.port; 186 } else { 187 endpoint.host = endpoint.hostname; 188 } 189 190 httpRequest.virtualHostedBucket = b; // needed for signing the request 191 service.removeVirtualHostedBucketFromPath(req); 192 } 193 } 194 }, 195 196 /** 197 * Takes the bucket name out of the path if bucket is virtual-hosted 198 * 199 * @api private 200 */ 201 removeVirtualHostedBucketFromPath: function removeVirtualHostedBucketFromPath(req) { 202 var httpRequest = req.httpRequest; 203 var bucket = httpRequest.virtualHostedBucket; 204 if (bucket && httpRequest.path) { 205 httpRequest.path = httpRequest.path.replace(new RegExp('/' + bucket), ''); 206 if (httpRequest.path[0] !== '/') { 207 httpRequest.path = '/' + httpRequest.path; 208 } 209 } 210 }, 211 212 /** 213 * Adds Expect: 100-continue header if payload is greater-or-equal 1MB 214 * @api private 215 */ 216 addExpect100Continue: function addExpect100Continue(req) { 217 var len = req.httpRequest.headers['Content-Length']; 218 if (AWS.util.isNode() && len >= 1024 * 1024) { 219 req.httpRequest.headers['Expect'] = '100-continue'; 220 } 221 }, 222 223 /** 224 * Adds a default content type if none is supplied. 225 * 226 * @api private 227 */ 228 addContentType: function addContentType(req) { 229 var httpRequest = req.httpRequest; 230 if (httpRequest.method === 'GET' || httpRequest.method === 'HEAD') { 231 // Content-Type is not set in GET/HEAD requests 232 delete httpRequest.headers['Content-Type']; 233 return; 234 } 235 236 if (!httpRequest.headers['Content-Type']) { // always have a Content-Type 237 httpRequest.headers['Content-Type'] = 'application/octet-stream'; 238 } 239 240 var contentType = httpRequest.headers['Content-Type']; 241 if (AWS.util.isBrowser()) { 242 if (typeof httpRequest.body === 'string' && !contentType.match(/;\s*charset=/)) { 243 var charset = '; charset=UTF-8'; 244 httpRequest.headers['Content-Type'] += charset; 245 } else { 246 var replaceFn = function(_, prefix, charsetName) { 247 return prefix + charsetName.toUpperCase(); 248 }; 249 250 httpRequest.headers['Content-Type'] = 251 contentType.replace(/(;\s*charset=)(.+)$/, replaceFn); 252 } 253 } 254 }, 255 256 /** 257 * @api private 258 */ 259 computableChecksumOperations: { 260 putBucketCors: true, 261 putBucketLifecycle: true, 262 putBucketLifecycleConfiguration: true, 263 putBucketTagging: true, 264 deleteObjects: true, 265 putBucketReplication: true 266 }, 267 268 /** 269 * Checks whether checksums should be computed for the request. 270 * If the request requires checksums to be computed, this will always 271 * return true, otherwise it depends on whether {AWS.Config.computeChecksums} 272 * is set. 273 * 274 * @param req [AWS.Request] the request to check against 275 * @return [Boolean] whether to compute checksums for a request. 276 * @api private 277 */ 278 willComputeChecksums: function willComputeChecksums(req) { 279 if (this.computableChecksumOperations[req.operation]) return true; 280 if (!this.config.computeChecksums) return false; 281 282 // TODO: compute checksums for Stream objects 283 if (!AWS.util.Buffer.isBuffer(req.httpRequest.body) && 284 typeof req.httpRequest.body !== 'string') { 285 return false; 286 } 287 288 var rules = req.service.api.operations[req.operation].input.members; 289 290 // Sha256 signing disabled, and not a presigned url 291 if (req.service.shouldDisableBodySigning(req) && !Object.prototype.hasOwnProperty.call(req.httpRequest.headers, 'presigned-expires')) { 292 if (rules.ContentMD5 && !req.params.ContentMD5) { 293 return true; 294 } 295 } 296 297 // V4 signer uses SHA256 signatures so only compute MD5 if it is required 298 if (req.service.getSignerClass(req) === AWS.Signers.V4) { 299 if (rules.ContentMD5 && !rules.ContentMD5.required) return false; 300 } 301 302 if (rules.ContentMD5 && !req.params.ContentMD5) return true; 303 }, 304 305 /** 306 * A listener that computes the Content-MD5 and sets it in the header. 307 * @see AWS.S3.willComputeChecksums 308 * @api private 309 */ 310 computeContentMd5: function computeContentMd5(req) { 311 if (req.service.willComputeChecksums(req)) { 312 var md5 = AWS.util.crypto.md5(req.httpRequest.body, 'base64'); 313 req.httpRequest.headers['Content-MD5'] = md5; 314 } 315 }, 316 317 /** 318 * @api private 319 */ 320 computeSseCustomerKeyMd5: function computeSseCustomerKeyMd5(req) { 321 var keys = { 322 SSECustomerKey: 'x-amz-server-side-encryption-customer-key-MD5', 323 CopySourceSSECustomerKey: 'x-amz-copy-source-server-side-encryption-customer-key-MD5' 324 }; 325 AWS.util.each(keys, function(key, header) { 326 if (req.params[key]) { 327 var value = AWS.util.crypto.md5(req.params[key], 'base64'); 328 req.httpRequest.headers[header] = value; 329 } 330 }); 331 }, 332 333 /** 334 * Returns true if the bucket name should be left in the URI path for 335 * a request to S3. This function takes into account the current 336 * endpoint protocol (e.g. http or https). 337 * 338 * @api private 339 */ 340 pathStyleBucketName: function pathStyleBucketName(bucketName) { 341 // user can force path style requests via the configuration 342 if (this.config.s3ForcePathStyle) return true; 343 if (this.config.s3BucketEndpoint) return false; 344 345 if (this.dnsCompatibleBucketName(bucketName)) { 346 return (this.config.sslEnabled && bucketName.match(/\./)) ? true : false; 347 } else { 348 return true; // not dns compatible names must always use path style 349 } 350 }, 351 352 /** 353 * Returns true if the bucket name is DNS compatible. Buckets created 354 * outside of the classic region MUST be DNS compatible. 355 * 356 * @api private 357 */ 358 dnsCompatibleBucketName: function dnsCompatibleBucketName(bucketName) { 359 var b = bucketName; 360 var domain = new RegExp(/^[a-z0-9][a-z0-9\.\-]{1,61}[a-z0-9]$/); 361 var ipAddress = new RegExp(/(\d+\.){3}\d+/); 362 var dots = new RegExp(/\.\./); 363 return (b.match(domain) && !b.match(ipAddress) && !b.match(dots)) ? true : false; 364 }, 365 366 /** 367 * @return [Boolean] whether response contains an error 368 * @api private 369 */ 370 successfulResponse: function successfulResponse(resp) { 371 var req = resp.request; 372 var httpResponse = resp.httpResponse; 373 if (operationsWith200StatusCodeError[req.operation] && 374 httpResponse.body.toString().match('<Error>')) { 375 return false; 376 } else { 377 return httpResponse.statusCode < 300; 378 } 379 }, 380 381 /** 382 * @return [Boolean] whether the error can be retried 383 * @api private 384 */ 385 retryableError: function retryableError(error, request) { 386 if (operationsWith200StatusCodeError[request.operation] && 387 error.statusCode === 200) { 388 return true; 389 } else if (request._requestRegionForBucket && 390 request.service.bucketRegionCache[request._requestRegionForBucket]) { 391 return false; 392 } else if (error && error.code === 'RequestTimeout') { 393 return true; 394 } else if (error && 395 regionRedirectErrorCodes.indexOf(error.code) != -1 && 396 error.region && error.region != request.httpRequest.region) { 397 request.httpRequest.region = error.region; 398 if (error.statusCode === 301) { 399 request.service.updateReqBucketRegion(request); 400 } 401 return true; 402 } else { 403 var _super = AWS.Service.prototype.retryableError; 404 return _super.call(this, error, request); 405 } 406 }, 407 408 /** 409 * Updates httpRequest with region. If region is not provided, then 410 * the httpRequest will be updated based on httpRequest.region 411 * 412 * @api private 413 */ 414 updateReqBucketRegion: function updateReqBucketRegion(request, region) { 415 var httpRequest = request.httpRequest; 416 if (typeof region === 'string' && region.length) { 417 httpRequest.region = region; 418 } 419 if (!httpRequest.endpoint.host.match(/s3(?!-accelerate).*\.amazonaws\.com$/)) { 420 return; 421 } 422 var service = request.service; 423 var s3Config = service.config; 424 var s3BucketEndpoint = s3Config.s3BucketEndpoint; 425 if (s3BucketEndpoint) { 426 delete s3Config.s3BucketEndpoint; 427 } 428 var newConfig = AWS.util.copy(s3Config); 429 delete newConfig.endpoint; 430 newConfig.region = httpRequest.region; 431 432 httpRequest.endpoint = (new AWS.S3(newConfig)).endpoint; 433 service.populateURI(request); 434 s3Config.s3BucketEndpoint = s3BucketEndpoint; 435 httpRequest.headers.Host = httpRequest.endpoint.host; 436 437 if (request._asm.currentState === 'validate') { 438 request.removeListener('build', service.populateURI); 439 request.addListener('build', service.removeVirtualHostedBucketFromPath); 440 } 441 }, 442 443 /** 444 * Provides a specialized parser for getBucketLocation -- all other 445 * operations are parsed by the super class. 446 * 447 * @api private 448 */ 449 extractData: function extractData(resp) { 450 var req = resp.request; 451 if (req.operation === 'getBucketLocation') { 452 var match = resp.httpResponse.body.toString().match(/>(.+)<\/Location/); 453 delete resp.data['_']; 454 if (match) { 455 resp.data.LocationConstraint = match[1]; 456 } else { 457 resp.data.LocationConstraint = ''; 458 } 459 } 460 var bucket = req.params.Bucket || null; 461 if (req.operation === 'deleteBucket' && typeof bucket === 'string' && !resp.error) { 462 req.service.clearBucketRegionCache(bucket); 463 } else { 464 var headers = resp.httpResponse.headers || {}; 465 var region = headers['x-amz-bucket-region'] || null; 466 if (!region && req.operation === 'createBucket' && !resp.error) { 467 var createBucketConfiguration = req.params.CreateBucketConfiguration; 468 if (!createBucketConfiguration) { 469 region = 'us-east-1'; 470 } else if (createBucketConfiguration.LocationConstraint === 'EU') { 471 region = 'eu-west-1'; 472 } else { 473 region = createBucketConfiguration.LocationConstraint; 474 } 475 } 476 if (region) { 477 if (bucket && region !== req.service.bucketRegionCache[bucket]) { 478 req.service.bucketRegionCache[bucket] = region; 479 } 480 } 481 } 482 req.service.extractRequestIds(resp); 483 }, 484 485 /** 486 * Extracts an error object from the http response. 487 * 488 * @api private 489 */ 490 extractError: function extractError(resp) { 491 var codes = { 492 304: 'NotModified', 493 403: 'Forbidden', 494 400: 'BadRequest', 495 404: 'NotFound' 496 }; 497 498 var req = resp.request; 499 var code = resp.httpResponse.statusCode; 500 var body = resp.httpResponse.body || ''; 501 502 var headers = resp.httpResponse.headers || {}; 503 var region = headers['x-amz-bucket-region'] || null; 504 var bucket = req.params.Bucket || null; 505 var bucketRegionCache = req.service.bucketRegionCache; 506 if (region && bucket && region !== bucketRegionCache[bucket]) { 507 bucketRegionCache[bucket] = region; 508 } 509 510 var cachedRegion; 511 if (codes[code] && body.length === 0) { 512 if (bucket && !region) { 513 cachedRegion = bucketRegionCache[bucket] || null; 514 if (cachedRegion !== req.httpRequest.region) { 515 region = cachedRegion; 516 } 517 } 518 resp.error = AWS.util.error(new Error(), { 519 code: codes[code], 520 message: null, 521 region: region 522 }); 523 } else { 524 var data = new AWS.XML.Parser().parse(body.toString()); 525 526 if (data.Region && !region) { 527 region = data.Region; 528 if (bucket && region !== bucketRegionCache[bucket]) { 529 bucketRegionCache[bucket] = region; 530 } 531 } else if (bucket && !region && !data.Region) { 532 cachedRegion = bucketRegionCache[bucket] || null; 533 if (cachedRegion !== req.httpRequest.region) { 534 region = cachedRegion; 535 } 536 } 537 538 resp.error = AWS.util.error(new Error(), { 539 code: data.Code || code, 540 message: data.Message || null, 541 region: region 542 }); 543 } 544 req.service.extractRequestIds(resp); 545 }, 546 547 /** 548 * If region was not obtained synchronously, then send async request 549 * to get bucket region for errors resulting from wrong region. 550 * 551 * @api private 552 */ 553 requestBucketRegion: function requestBucketRegion(resp, done) { 554 var error = resp.error; 555 var req = resp.request; 556 var bucket = req.params.Bucket || null; 557 558 if (!error || !bucket || error.region || req.operation === 'listObjects' || 559 (AWS.util.isNode() && req.operation === 'headBucket') || 560 (error.statusCode === 400 && req.operation !== 'headObject') || 561 regionRedirectErrorCodes.indexOf(error.code) === -1) { 562 return done(); 563 } 564 var reqOperation = AWS.util.isNode() ? 'headBucket' : 'listObjects'; 565 var reqParams = {Bucket: bucket}; 566 if (reqOperation === 'listObjects') reqParams.MaxKeys = 0; 567 var regionReq = req.service[reqOperation](reqParams); 568 regionReq._requestRegionForBucket = bucket; 569 regionReq.send(function() { 570 var region = req.service.bucketRegionCache[bucket] || null; 571 error.region = region; 572 done(); 573 }); 574 }, 575 576 /** 577 * For browser only. If NetworkingError received, will attempt to obtain 578 * the bucket region. 579 * 580 * @api private 581 */ 582 reqRegionForNetworkingError: function reqRegionForNetworkingError(resp, done) { 583 if (!AWS.util.isBrowser()) { 584 return done(); 585 } 586 var error = resp.error; 587 var request = resp.request; 588 var bucket = request.params.Bucket; 589 if (!error || error.code !== 'NetworkingError' || !bucket || 590 request.httpRequest.region === 'us-east-1') { 591 return done(); 592 } 593 var service = request.service; 594 var bucketRegionCache = service.bucketRegionCache; 595 var cachedRegion = bucketRegionCache[bucket] || null; 596 597 if (cachedRegion && cachedRegion !== request.httpRequest.region) { 598 service.updateReqBucketRegion(request, cachedRegion); 599 done(); 600 } else if (!service.dnsCompatibleBucketName(bucket)) { 601 service.updateReqBucketRegion(request, 'us-east-1'); 602 if (bucketRegionCache[bucket] !== 'us-east-1') { 603 bucketRegionCache[bucket] = 'us-east-1'; 604 } 605 done(); 606 } else if (request.httpRequest.virtualHostedBucket) { 607 var getRegionReq = service.listObjects({Bucket: bucket, MaxKeys: 0}); 608 service.updateReqBucketRegion(getRegionReq, 'us-east-1'); 609 getRegionReq._requestRegionForBucket = bucket; 610 611 getRegionReq.send(function() { 612 var region = service.bucketRegionCache[bucket] || null; 613 if (region && region !== request.httpRequest.region) { 614 service.updateReqBucketRegion(request, region); 615 } 616 done(); 617 }); 618 } else { 619 // DNS-compatible path-style 620 // (s3ForcePathStyle or bucket name with dot over https) 621 // Cannot obtain region information for this case 622 done(); 623 } 624 }, 625 626 /** 627 * Cache for bucket region. 628 * 629 * @api private 630 */ 631 bucketRegionCache: {}, 632 633 /** 634 * Clears bucket region cache. 635 * 636 * @api private 637 */ 638 clearBucketRegionCache: function(buckets) { 639 var bucketRegionCache = this.bucketRegionCache; 640 if (!buckets) { 641 buckets = Object.keys(bucketRegionCache); 642 } else if (typeof buckets === 'string') { 643 buckets = [buckets]; 644 } 645 for (var i = 0; i < buckets.length; i++) { 646 delete bucketRegionCache[buckets[i]]; 647 } 648 return bucketRegionCache; 649 }, 650 651 /** 652 * Corrects request region if bucket's cached region is different 653 * 654 * @api private 655 */ 656 correctBucketRegionFromCache: function correctBucketRegionFromCache(req) { 657 var bucket = req.params.Bucket || null; 658 if (bucket) { 659 var service = req.service; 660 var requestRegion = req.httpRequest.region; 661 var cachedRegion = service.bucketRegionCache[bucket]; 662 if (cachedRegion && cachedRegion !== requestRegion) { 663 service.updateReqBucketRegion(req, cachedRegion); 664 } 665 } 666 }, 667 668 /** 669 * Extracts S3 specific request ids from the http response. 670 * 671 * @api private 672 */ 673 extractRequestIds: function extractRequestIds(resp) { 674 var extendedRequestId = resp.httpResponse.headers ? resp.httpResponse.headers['x-amz-id-2'] : null; 675 var cfId = resp.httpResponse.headers ? resp.httpResponse.headers['x-amz-cf-id'] : null; 676 resp.extendedRequestId = extendedRequestId; 677 resp.cfId = cfId; 678 679 if (resp.error) { 680 resp.error.requestId = resp.requestId || null; 681 resp.error.extendedRequestId = extendedRequestId; 682 resp.error.cfId = cfId; 683 } 684 }, 685 686 /** 687 * Get a pre-signed URL for a given operation name. 688 * 689 * @note You must ensure that you have static or previously resolved 690 * credentials if you call this method synchronously (with no callback), 691 * otherwise it may not properly sign the request. If you cannot guarantee 692 * this (you are using an asynchronous credential provider, i.e., EC2 693 * IAM roles), you should always call this method with an asynchronous 694 * callback. 695 * @param operation [String] the name of the operation to call 696 * @param params [map] parameters to pass to the operation. See the given 697 * operation for the expected operation parameters. In addition, you can 698 * also pass the "Expires" parameter to inform S3 how long the URL should 699 * work for. 700 * @option params Expires [Integer] (900) the number of seconds to expire 701 * the pre-signed URL operation in. Defaults to 15 minutes. 702 * @param callback [Function] if a callback is provided, this function will 703 * pass the URL as the second parameter (after the error parameter) to 704 * the callback function. 705 * @return [String] if called synchronously (with no callback), returns the 706 * signed URL. 707 * @return [null] nothing is returned if a callback is provided. 708 * @example Pre-signing a getObject operation (synchronously) 709 * var params = {Bucket: 'bucket', Key: 'key'}; 710 * var url = s3.getSignedUrl('getObject', params); 711 * console.log('The URL is', url); 712 * @example Pre-signing a putObject (asynchronously) 713 * var params = {Bucket: 'bucket', Key: 'key'}; 714 * s3.getSignedUrl('putObject', params, function (err, url) { 715 * console.log('The URL is', url); 716 * }); 717 * @example Pre-signing a putObject operation with a specific payload 718 * var params = {Bucket: 'bucket', Key: 'key', Body: 'body'}; 719 * var url = s3.getSignedUrl('putObject', params); 720 * console.log('The URL is', url); 721 * @example Passing in a 1-minute expiry time for a pre-signed URL 722 * var params = {Bucket: 'bucket', Key: 'key', Expires: 60}; 723 * var url = s3.getSignedUrl('getObject', params); 724 * console.log('The URL is', url); // expires in 60 seconds 725 */ 726 getSignedUrl: function getSignedUrl(operation, params, callback) { 727 params = AWS.util.copy(params || {}); 728 var expires = params.Expires || 900; 729 delete params.Expires; // we can't validate this 730 var request = this.makeRequest(operation, params); 731 return request.presign(expires, callback); 732 }, 733 734 /** 735 * @api private 736 */ 737 prepareSignedUrl: function prepareSignedUrl(request) { 738 request.addListener('validate', request.service.noPresignedContentLength); 739 request.removeListener('build', request.service.addContentType); 740 if (!request.params.Body) { 741 // no Content-MD5/SHA-256 if body is not provided 742 request.removeListener('build', request.service.computeContentMd5); 743 } else { 744 request.addListener('afterBuild', AWS.EventListeners.Core.COMPUTE_SHA256); 745 } 746 }, 747 748 /** 749 * @api private 750 * @param request 751 */ 752 disableBodySigning: function disableBodySigning(request) { 753 var headers = request.httpRequest.headers; 754 // Add the header to anything that isn't a presigned url, unless that presigned url had a body defined 755 if (!Object.prototype.hasOwnProperty.call(headers, 'presigned-expires')) { 756 headers['X-Amz-Content-Sha256'] = 'UNSIGNED-PAYLOAD'; 757 } 758 }, 759 760 /** 761 * @api private 762 */ 763 noPresignedContentLength: function noPresignedContentLength(request) { 764 if (request.params.ContentLength !== undefined) { 765 throw AWS.util.error(new Error(), {code: 'UnexpectedParameter', 766 message: 'ContentLength is not supported in pre-signed URLs.'}); 767 } 768 }, 769 770 createBucket: function createBucket(params, callback) { 771 // When creating a bucket *outside* the classic region, the location 772 // constraint must be set for the bucket and it must match the endpoint. 773 // This chunk of code will set the location constraint param based 774 // on the region (when possible), but it will not override a passed-in 775 // location constraint. 776 if (typeof params === 'function' || !params) { 777 callback = callback || params; 778 params = {}; 779 } 780 var hostname = this.endpoint.hostname; 781 if (hostname !== this.api.globalEndpoint && !params.CreateBucketConfiguration) { 782 params.CreateBucketConfiguration = { LocationConstraint: this.config.region }; 783 } 784 return this.makeRequest('createBucket', params, callback); 785 }, 786 787 /** 788 * @overload upload(params = {}, [options], [callback]) 789 * Uploads an arbitrarily sized buffer, blob, or stream, using intelligent 790 * concurrent handling of parts if the payload is large enough. You can 791 * configure the concurrent queue size by setting `options`. Note that this 792 * is the only operation for which the SDK can retry requests with stream 793 * bodies. 794 * 795 * @param (see AWS.S3.putObject) 796 * @option (see AWS.S3.ManagedUpload.constructor) 797 * @return [AWS.S3.ManagedUpload] the managed upload object that can call 798 * `send()` or track progress. 799 * @example Uploading a stream object 800 * var params = {Bucket: 'bucket', Key: 'key', Body: stream}; 801 * s3.upload(params, function(err, data) { 802 * console.log(err, data); 803 * }); 804 * @example Uploading a stream with concurrency of 1 and partSize of 10mb 805 * var params = {Bucket: 'bucket', Key: 'key', Body: stream}; 806 * var options = {partSize: 10 * 1024 * 1024, queueSize: 1}; 807 * s3.upload(params, options, function(err, data) { 808 * console.log(err, data); 809 * }); 810 * @callback callback function(err, data) 811 * @param err [Error] an error or null if no error occurred. 812 * @param data [map] The response data from the successful upload: 813 * * `Location` (String) the URL of the uploaded object 814 * * `ETag` (String) the ETag of the uploaded object 815 * * `Bucket` (String) the bucket to which the object was uploaded 816 * * `Key` (String) the key to which the object was uploaded 817 * @see AWS.S3.ManagedUpload 818 */ 819 upload: function upload(params, options, callback) { 820 if (typeof options === 'function' && callback === undefined) { 821 callback = options; 822 options = null; 823 } 824 825 options = options || {}; 826 options = AWS.util.merge(options || {}, {service: this, params: params}); 827 828 var uploader = new AWS.S3.ManagedUpload(options); 829 if (typeof callback === 'function') uploader.send(callback); 830 return uploader; 831 } 832 });