diff --git a/lib/api/bucketGet.js b/lib/api/bucketGet.js index d4e7e1b81b..22dc78fb94 100644 --- a/lib/api/bucketGet.js +++ b/lib/api/bucketGet.js @@ -1,3 +1,4 @@ +const { promisify } = require('util'); const querystring = require('querystring'); const { errors, errorInstances, versioning, s3middleware } = require('arsenal'); const constants = require('../../constants'); @@ -8,14 +9,9 @@ const escapeForXml = s3middleware.escapeForXml; const { pushMetric } = require('../utapi/utilities'); const versionIdUtils = versioning.VersionID; const monitoring = require('../utilities/monitoringHandler'); -const { generateToken, decryptToken } - = require('../api/apiUtils/object/continueToken'); +const { generateToken, decryptToken } = require('../api/apiUtils/object/continueToken'); -// do not url encode the continuation tokens -const skipUrlEncoding = new Set([ - 'ContinuationToken', - 'NextContinuationToken', -]); +const xmlParamsToSkipUrlEncoding = new Set(['ContinuationToken', 'NextContinuationToken']); /* Sample XML response for GET bucket objects V2: @@ -122,17 +118,16 @@ function processVersions(bucketName, listParams, list) { { tag: 'IsTruncated', value: isTruncated }, ]; - const escapeXmlFn = listParams.encoding === 'url' ? - querystring.escape : escapeForXml; + const escapeXmlFn = listParams.encoding === 'url' ? querystring.escape : escapeForXml; xmlParams.forEach(p => { if (p.value) { const val = p.tag !== 'NextVersionIdMarker' || p.value === 'null' ? - p.value : versionIdUtils.encode(p.value); + p.value : + versionIdUtils.encode(p.value); xml.push(`<${p.tag}>${escapeXmlFn(val)}`); } }); - let lastKey = listParams.keyMarker ? - escapeXmlFn(listParams.keyMarker) : undefined; + let lastKey = listParams.keyMarker ? escapeXmlFn(listParams.keyMarker) : undefined; list.Versions.forEach(item => { const v = item.value; const objectKey = escapeXmlFn(item.key); @@ -143,7 +138,8 @@ function processVersions(bucketName, listParams, list) { `${objectKey}`, '', (v.IsNull || v.VersionId === undefined) ? - 'null' : versionIdUtils.encode(v.VersionId), + 'null' + : versionIdUtils.encode(v.VersionId), '', `${isLatest}`, `${v.LastModified}`, @@ -182,31 +178,19 @@ function processMasterVersions(bucketName, listParams, list) { ]; if (listParams.v2) { - xmlParams.push( - { tag: 'StartAfter', value: listParams.startAfter || '' }); - xmlParams.push( - { tag: 'FetchOwner', value: `${listParams.fetchOwner}` }); - xmlParams.push({ - tag: 'ContinuationToken', - value: generateToken(listParams.continuationToken) || '', - }); - xmlParams.push({ - tag: 'NextContinuationToken', - value: generateToken(list.NextContinuationToken), - }); - xmlParams.push({ - tag: 'KeyCount', - value: list.Contents ? list.Contents.length : 0, - }); + xmlParams.push({ tag: 'StartAfter', value: listParams.startAfter || '' }); + xmlParams.push({ tag: 'FetchOwner', value: `${listParams.fetchOwner}` }); + xmlParams.push({ tag: 'ContinuationToken', value: generateToken(listParams.continuationToken) || '', }); + xmlParams.push({ tag: 'NextContinuationToken', value: generateToken(list.NextContinuationToken), }); + xmlParams.push({ tag: 'KeyCount', value: list.Contents ? list.Contents.length : 0, }); } else { xmlParams.push({ tag: 'Marker', value: listParams.marker || '' }); xmlParams.push({ tag: 'NextMarker', value: list.NextMarker }); } - const escapeXmlFn = listParams.encoding === 'url' ? - querystring.escape : escapeForXml; + const escapeXmlFn = listParams.encoding === 'url' ? querystring.escape : escapeForXml; xmlParams.forEach(p => { - if (p.value && skipUrlEncoding.has(p.tag)) { + if (p.value && xmlParamsToSkipUrlEncoding.has(p.tag)) { xml.push(`<${p.tag}>${p.value}`); } else if (p.value || p.tag === 'KeyCount' || p.tag === 'MaxKeys') { xml.push(`<${p.tag}>${escapeXmlFn(p.value)}`); @@ -246,15 +230,14 @@ function processMasterVersions(bucketName, listParams, list) { ); }); list.CommonPrefixes.forEach(item => { - const val = escapeXmlFn(item); - xml.push(`${val}`); + xml.push(`${escapeXmlFn(item)}`); }); xml.push(''); + return xml.join(''); } -function handleResult(listParams, requestMaxKeys, encoding, authInfo, - bucketName, list, corsHeaders, log, callback) { +function handleResult(listParams, requestMaxKeys, encoding, authInfo, bucketName, list, log) { // eslint-disable-next-line no-param-reassign listParams.maxKeys = requestMaxKeys; // eslint-disable-next-line no-param-reassign @@ -267,7 +250,7 @@ function handleResult(listParams, requestMaxKeys, encoding, authInfo, } pushMetric('listBucket', log, { authInfo, bucket: bucketName }); monitoring.promMetrics('GET', bucketName, '200', 'listBucket'); - return callback(null, res, corsHeaders); + return res; } /** @@ -278,28 +261,25 @@ function handleResult(listParams, requestMaxKeys, encoding, authInfo, * @param {function} log - Werelogs request logger * @param {function} callback - callback to respond to http request * with either error code or xml response body - * @return {undefined} + * @return {Promise} - object containing xml and additionalResHeaders */ -function bucketGet(authInfo, request, log, callback) { +async function bucketGet(authInfo, request, log) { const params = request.query; const bucketName = request.bucketName; const v2 = params['list-type']; + if (v2 !== undefined && Number.parseInt(v2, 10) !== 2) { - return callback(errorInstances.InvalidArgument.customizeDescription('Invalid ' + - 'List Type specified in Request')); + throw errorInstances.InvalidArgument.customizeDescription('Invalid List Type specified in Request'); } + if (v2) { - log.addDefaultFields({ - action: 'ListObjectsV2', - }); + log.addDefaultFields({ action: 'ListObjectsV2', }); if (request.serverAccessLog) { // eslint-disable-next-line no-param-reassign request.serverAccessLog.analyticsAction = 'ListObjectsV2'; } } else if (params.versions !== undefined) { - log.addDefaultFields({ - action: 'ListObjectVersions', - }); + log.addDefaultFields({ action: 'ListObjectVersions', }); if (request.serverAccessLog) { // eslint-disable-next-line no-param-reassign request.serverAccessLog.analyticsAction = 'ListObjectVersions'; @@ -308,21 +288,15 @@ function bucketGet(authInfo, request, log, callback) { log.debug('processing request', { method: 'bucketGet' }); const encoding = params['encoding-type']; if (encoding !== undefined && encoding !== 'url') { - monitoring.promMetrics( - 'GET', bucketName, 400, 'listBucket'); - return callback(errorInstances.InvalidArgument.customizeDescription('Invalid ' + - 'Encoding Method specified in Request')); + monitoring.promMetrics('GET', bucketName, 400, 'listBucket'); + throw errorInstances.InvalidArgument.customizeDescription('Invalid Encoding Method specified in Request'); } - const requestMaxKeys = params['max-keys'] ? - Number.parseInt(params['max-keys'], 10) : 1000; + + const requestMaxKeys = params['max-keys'] ? Number.parseInt(params['max-keys'], 10) : 1000; if (Number.isNaN(requestMaxKeys) || requestMaxKeys < 0) { - monitoring.promMetrics( - 'GET', bucketName, 400, 'listBucket'); - return callback(errors.InvalidArgument); + monitoring.promMetrics('GET', bucketName, 400, 'listBucket'); + throw errors.InvalidArgument; } - // AWS only returns 1000 keys even if max keys are greater. - // Max keys stated in response xml can be greater than actual - // keys returned. const actualMaxKeys = Math.min(constants.listingHardLimit, requestMaxKeys); const metadataValParams = { @@ -344,56 +318,74 @@ function bucketGet(authInfo, request, log, callback) { if (v2) { listParams.v2 = true; listParams.startAfter = params['start-after']; - listParams.continuationToken = - decryptToken(params['continuation-token']); + listParams.continuationToken = decryptToken(params['continuation-token']); listParams.fetchOwner = params['fetch-owner'] === 'true'; } else { listParams.marker = params.marker; } - standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => { - const corsHeaders = collectCorsHeaders(request.headers.origin, - request.method, bucket); - if (err) { - log.debug('error processing request', { error: err }); - monitoring.promMetrics( - 'GET', bucketName, err.code, 'listBucket'); - return callback(err, null, corsHeaders); - } - if (params.versions !== undefined) { - listParams.listingType = 'DelimiterVersions'; - delete listParams.marker; - listParams.keyMarker = params['key-marker']; - listParams.versionIdMarker = params['version-id-marker'] ? - versionIdUtils.decode(params['version-id-marker']) : undefined; - } - if (!requestMaxKeys) { - const emptyList = { - CommonPrefixes: [], - Contents: [], - Versions: [], - IsTruncated: false, - }; - return handleResult(listParams, requestMaxKeys, encoding, authInfo, - bucketName, emptyList, corsHeaders, log, callback); - } - return services.getObjectListing(bucketName, listParams, log, - (err, list) => { - if (err) { - log.debug('error processing request', { error: err }); - monitoring.promMetrics( - 'GET', bucketName, err.code, 'listBucket'); - return callback(err, null, corsHeaders); - } - return handleResult(listParams, requestMaxKeys, encoding, authInfo, - bucketName, list, corsHeaders, log, callback); - }); - }); - return undefined; + let error; + let bucket; + + try { + const standardMetadataValidateBucketPromised = promisify(standardMetadataValidateBucket); + bucket = await standardMetadataValidateBucketPromised(metadataValParams, request.actionImplicitDenies, log); + } catch (err) { + error = err; + } + + const corsHeaders = collectCorsHeaders(request.headers.origin, request.method, bucket); + + if (error) { + log.debug('error processing request', { error }); + monitoring.promMetrics('GET', bucketName, error.code, 'listBucket'); + error.additionalResHeaders = corsHeaders; + throw error; + } + if (params.versions !== undefined) { + listParams.listingType = 'DelimiterVersions'; + delete listParams.marker; + listParams.keyMarker = params['key-marker']; + listParams.versionIdMarker = params['version-id-marker'] ? + versionIdUtils.decode(params['version-id-marker']) : + undefined; + } + if (!requestMaxKeys) { + const emptyList = { + CommonPrefixes: [], + Contents: [], + Versions: [], + IsTruncated: false, + }; + const res = handleResult(listParams, requestMaxKeys, encoding, authInfo, bucketName, emptyList, log); + return [res, corsHeaders]; + } + + let list; + try { + list = await promisify(services.getObjectListing)(bucketName, listParams, log); + } catch (err) { + log.debug('error processing request', { error: err }); + monitoring.promMetrics('GET', bucketName, err.code, 'listBucket'); + + err.additionalResHeaders = corsHeaders; + throw err; + } + + const res = handleResult(listParams, requestMaxKeys, encoding, authInfo, bucketName, list, log); + return [res, corsHeaders]; } module.exports = { processVersions, processMasterVersions, - bucketGet, + bucketGet: (...args) => { + const callback = args.at(-1); + const argsWithoutCallback = args.slice(0, -1); + + bucketGet(...argsWithoutCallback) + .then(result => callback(null, ...result)) + .catch(err => callback(err, null, err.additionalResHeaders)); + }, }; + diff --git a/lib/api/objectGetLegalHold.js b/lib/api/objectGetLegalHold.js index 2f165748f3..5fd5c62e93 100644 --- a/lib/api/objectGetLegalHold.js +++ b/lib/api/objectGetLegalHold.js @@ -1,4 +1,3 @@ -const async = require('async'); const { errors, errorInstances, s3middleware } = require('arsenal'); const { decodeVersionId, getVersionIdResHeader } @@ -10,31 +9,34 @@ const collectCorsHeaders = require('../utilities/collectCorsHeaders'); const { convertToXml } = s3middleware.objectLegalHold; +const standardMetadataValidateBucketAndObjPromised = (metadataValParams, actionImplicitDenies, log) => + new Promise((resolve, reject) => { + standardMetadataValidateBucketAndObj(metadataValParams, actionImplicitDenies, log, (err, bucket, objectMD) => { + if (err) {return reject(err);} + return resolve({ bucket, objectMD }); + }); + }); + /** * Returns legal hold status of object * @param {AuthInfo} authInfo - Instance of AuthInfo class with requester's info * @param {object} request - http request object * @param {object} log - Werelogs logger - * @param {function} callback - callback to server - * @return {undefined} + * @return {Promise} - object containing xml and additionalResHeaders */ -function objectGetLegalHold(authInfo, request, log, callback) { +async function objectGetLegalHold(authInfo, request, log) { log.debug('processing request', { method: 'objectGetLegalHold' }); const { bucketName, objectKey, query } = request; const decodedVidResult = decodeVersionId(query); if (decodedVidResult instanceof Error) { - log.trace('invalid versionId query', { - versionId: query.versionId, - error: decodedVidResult, - }); - return process.nextTick(() => callback(decodedVidResult)); + log.trace('invalid versionId query', { versionId: query.versionId, error: decodedVidResult }); + throw decodedVidResult; } const versionId = decodedVidResult; - // FIXME pass 'getDeleteMarker: true' option to set - // 'x-amz-delete-marker' header (see S3C-7592) + // FIXME pass 'getDeleteMarker: true' option to set 'x-amz-delete-marker' header (see S3C-7592) const metadataValParams = { authInfo, bucketName, @@ -44,71 +46,68 @@ function objectGetLegalHold(authInfo, request, log, callback) { request, }; - return async.waterfall([ - next => standardMetadataValidateBucketAndObj(metadataValParams, request.actionImplicitDenies, log, - (err, bucket, objectMD) => { - if (err) { - log.trace('request authorization failed', - { method: 'objectGetLegalHold', error: err }); - return next(err); - } - if (!objectMD) { - const err = versionId ? errors.NoSuchVersion : - errors.NoSuchKey; - log.trace('error no object metadata found', - { method: 'objectGetLegalHold', error: err }); - return next(err, bucket); - } - if (objectMD.isDeleteMarker) { - if (versionId) { - log.trace('requested version is delete marker', - { method: 'objectGetLegalHold' }); - // FIXME we should return a `x-amz-delete-marker: true` header, - // see S3C-7592 - return next(errors.MethodNotAllowed); - } - log.trace('most recent version is delete marker', - { method: 'objectGetLegalHold' }); - // FIXME we should return a `x-amz-delete-marker: true` header, - // see S3C-7592 - return next(errors.NoSuchKey); - } - if (!bucket.isObjectLockEnabled()) { - log.trace('object lock not enabled on bucket', - { method: 'objectGetRetention' }); - return next(errorInstances.InvalidRequest.customizeDescription( - 'Bucket is missing Object Lock Configuration')); - } - return next(null, bucket, objectMD); - }), - (bucket, objectMD, next) => { - const { legalHold } = objectMD; - const xml = convertToXml(legalHold); - if (xml === '') { - return next(errors.NoSuchObjectLockConfiguration); - } - return next(null, bucket, xml, objectMD); - }, - ], (err, bucket, xml, objectMD) => { - const additionalResHeaders = collectCorsHeaders(request.headers.origin, - request.method, bucket); - if (err) { - log.trace('error processing request', { error: err, - method: 'objectGetLegalHold' }); - } else { - pushMetric('getObjectLegalHold', log, { - authInfo, - bucket: bucketName, - keys: [objectKey], - versionId: objectMD ? objectMD.versionId : undefined, - location: objectMD ? objectMD.dataStoreName : undefined, - }); - const verCfg = bucket.getVersioningConfiguration(); - additionalResHeaders['x-amz-version-id'] = - getVersionIdResHeader(verCfg, objectMD); + let bucket, objectMD; + + try { + ({ bucket, objectMD } = await standardMetadataValidateBucketAndObjPromised( + metadataValParams, + request.actionImplicitDenies, + log, + )); + } catch (err) { + log.trace('request authorization failed', { method: 'objectGetLegalHold', error: err }); + throw err; + } + + if (!objectMD) { + const err = versionId ? errors.NoSuchVersion : errors.NoSuchKey; + log.trace('error no object metadata found', { method: 'objectGetLegalHold', error: err }); + throw err; + } + + if (objectMD.isDeleteMarker) { + if (versionId) { + log.trace('requested version is delete marker', { method: 'objectGetLegalHold' }); + // FIXME we should return a `x-amz-delete-marker: true` header, see S3C-7592 + throw errors.MethodNotAllowed; } - return callback(err, xml, additionalResHeaders); + + log.trace('most recent version is delete marker', { method: 'objectGetLegalHold' }); + // FIXME we should return a `x-amz-delete-marker: true` header, see S3C-7592 + throw errors.NoSuchKey; + } + + if (!bucket.isObjectLockEnabled()) { + log.trace('object lock not enabled on bucket', { method: 'objectGetRetention' }); + throw errorInstances.InvalidRequest.customizeDescription('Bucket is missing Object Lock Configuration'); + } + + const { legalHold } = objectMD; + const xml = convertToXml(legalHold); + if (xml === '') { + throw errors.NoSuchObjectLockConfiguration; + } + + const additionalResHeaders = collectCorsHeaders(request.headers.origin, request.method, bucket); + + pushMetric('getObjectLegalHold', log, { + authInfo, + bucket: bucketName, + keys: [objectKey], + versionId: objectMD ? objectMD.versionId : undefined, + location: objectMD ? objectMD.dataStoreName : undefined, }); + const verCfg = bucket.getVersioningConfiguration(); + additionalResHeaders['x-amz-version-id'] = getVersionIdResHeader(verCfg, objectMD); + + return [xml, additionalResHeaders]; } -module.exports = objectGetLegalHold; +module.exports = (...args) => { + const callback = args.at(-1); + const argsWithoutCallback = args.slice(0, -1); + + objectGetLegalHold(...argsWithoutCallback) + .then(result => callback(null, ...result)) + .catch(err => callback(err, null, err.additionalResHeaders)); +};