Compare commits
1 Commits
developmen
...
rf/extract
Author | SHA1 | Date |
---|---|---|
Electra Chong | 88099c35fb |
4
index.js
4
index.js
|
@ -49,6 +49,10 @@ module.exports = {
|
||||||
RESTClient: require('./lib/network/rest/RESTClient'),
|
RESTClient: require('./lib/network/rest/RESTClient'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
s3routes: {
|
||||||
|
routes: require('./lib/s3routes/routes'),
|
||||||
|
routesUtils: require('./lib/s3routes/routesUtils'),
|
||||||
|
},
|
||||||
storage: {
|
storage: {
|
||||||
metadata: {
|
metadata: {
|
||||||
MetadataFileServer:
|
MetadataFileServer:
|
||||||
|
|
|
@ -0,0 +1,141 @@
|
||||||
|
const errors = require('../errors');
|
||||||
|
|
||||||
|
const routeGET = require('./routes/routeGET');
|
||||||
|
const routePUT = require('./routes/routePUT');
|
||||||
|
const routeDELETE = require('./routes/routeDELETE');
|
||||||
|
const routeHEAD = require('./routes/routeHEAD');
|
||||||
|
const routePOST = require('./routes/routePOST');
|
||||||
|
const routeOPTIONS = require('./routes/routeOPTIONS');
|
||||||
|
const routesUtils = require('./routesUtils');
|
||||||
|
const routeWebsite = require('./routes/routeWebsite');
|
||||||
|
|
||||||
|
const routeMap = {
|
||||||
|
GET: routeGET,
|
||||||
|
PUT: routePUT,
|
||||||
|
POST: routePOST,
|
||||||
|
DELETE: routeDELETE,
|
||||||
|
HEAD: routeHEAD,
|
||||||
|
OPTIONS: routeOPTIONS,
|
||||||
|
};
|
||||||
|
|
||||||
|
function checkUnsupportedRoutes(reqMethod, reqQuery, unsupportedQueries, log) {
|
||||||
|
const method = routeMap[reqMethod];
|
||||||
|
if (!method) {
|
||||||
|
return { error: errors.MethodNotAllowed };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (routesUtils.isUnsupportedQuery(reqQuery, unsupportedQueries)) {
|
||||||
|
log.debug('encountered unsupported query');
|
||||||
|
return { error: errors.NotImplemented };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { method };
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkBucketAndKey(bucketName, objectKey, method, reqQuery,
|
||||||
|
isValidBucketName, log) {
|
||||||
|
// if empty name and request not a list Buckets
|
||||||
|
if (!bucketName && !(method === 'GET' && !objectKey)) {
|
||||||
|
console.log('bucketName:', bucketName);
|
||||||
|
console.log('method:', method)
|
||||||
|
console.log('objectKey:', objectKey)
|
||||||
|
log.debug('empty bucket name', { method: 'routes' });
|
||||||
|
return (method !== 'OPTIONS') ?
|
||||||
|
errors.MethodNotAllowed : errors.AccessForbidden
|
||||||
|
.customizeDescription('CORSResponse: Bucket not found');
|
||||||
|
}
|
||||||
|
if (bucketName !== undefined &&
|
||||||
|
isValidBucketName(bucketName) === false) {
|
||||||
|
log.debug('invalid bucket name', { bucketName });
|
||||||
|
return errors.InvalidBucketName;
|
||||||
|
}
|
||||||
|
if ((reqQuery.partNumber || reqQuery.uploadId)
|
||||||
|
&& objectKey === undefined) {
|
||||||
|
return errors.InvalidRequest
|
||||||
|
.customizeDescription('A key must be specified');
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function routes(req, res, params, logger) {
|
||||||
|
const {
|
||||||
|
api,
|
||||||
|
healthcheckHandler,
|
||||||
|
statsClient,
|
||||||
|
allEndpoints,
|
||||||
|
websiteEndpoints,
|
||||||
|
isValidBucketName,
|
||||||
|
unsupportedQueries,
|
||||||
|
// eslint-disable-next-line
|
||||||
|
dataRetrievalFn,
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
const clientInfo = {
|
||||||
|
clientIP: req.socket.remoteAddress,
|
||||||
|
clientPort: req.socket.remotePort,
|
||||||
|
httpMethod: req.method,
|
||||||
|
httpURL: req.url,
|
||||||
|
endpoint: req.endpoint,
|
||||||
|
};
|
||||||
|
|
||||||
|
const log = logger.newRequestLogger();
|
||||||
|
log.info('received request', clientInfo);
|
||||||
|
|
||||||
|
log.end().addDefaultFields(clientInfo);
|
||||||
|
|
||||||
|
if (req.url === '/_/healthcheck') {
|
||||||
|
return healthcheckHandler(clientInfo.clientIP, false, req, res, log,
|
||||||
|
statsClient);
|
||||||
|
} else if (req.url === '/_/healthcheck/deep') {
|
||||||
|
return healthcheckHandler(clientInfo.clientIP, true, req, res, log);
|
||||||
|
}
|
||||||
|
if (statsClient) {
|
||||||
|
// report new request for stats
|
||||||
|
statsClient.reportNewRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const validHosts = allEndpoints.concat(websiteEndpoints);
|
||||||
|
routesUtils.normalizeRequest(req, validHosts);
|
||||||
|
} catch (err) {
|
||||||
|
console.log('err normalizing request', err);
|
||||||
|
log.trace('could not normalize request', { error: err.stack });
|
||||||
|
return routesUtils.responseXMLBody(
|
||||||
|
errors.InvalidURI, undefined, res, log);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.addDefaultFields({
|
||||||
|
bucketName: req.bucketName,
|
||||||
|
objectKey: req.objectKey,
|
||||||
|
bytesReceived: req.parsedContentLength || 0,
|
||||||
|
bodyLength: parseInt(req.headers['content-length'], 10) || 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const reqMethod = req.method.toUpperCase();
|
||||||
|
|
||||||
|
const { error, method } = checkUnsupportedRoutes(reqMethod, req.query,
|
||||||
|
unsupportedQueries, statsClient, log);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
log.trace('error validating route or uri params', { error });
|
||||||
|
return routesUtils.responseXMLBody(error, null, res, log);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bucketOrKeyError = checkBucketAndKey(req.bucketName, req.objectKey,
|
||||||
|
reqMethod, req.query, isValidBucketName, log);
|
||||||
|
|
||||||
|
if (bucketOrKeyError) {
|
||||||
|
log.trace('error with bucket or key value',
|
||||||
|
{ error: bucketOrKeyError });
|
||||||
|
return routesUtils.responseXMLBody(bucketOrKeyError, null, res, log);
|
||||||
|
}
|
||||||
|
|
||||||
|
// bucket website request
|
||||||
|
if (websiteEndpoints && websiteEndpoints.indexOf(req.parsedHost) > -1) {
|
||||||
|
return routeWebsite(req, res, api, log, statsClient, dataRetrievalFn);
|
||||||
|
}
|
||||||
|
|
||||||
|
return method(req, res, api, log, statsClient, dataRetrievalFn);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = routes;
|
|
@ -0,0 +1,71 @@
|
||||||
|
const routesUtils = require('../routesUtils');
|
||||||
|
const errors = require('../../errors');
|
||||||
|
|
||||||
|
function routeDELETE(request, response, api, log, statsClient) {
|
||||||
|
log.debug('routing request', { method: 'routeDELETE' });
|
||||||
|
|
||||||
|
if (request.query.uploadId) {
|
||||||
|
if (request.objectKey === undefined) {
|
||||||
|
return routesUtils.responseNoBody(
|
||||||
|
errors.InvalidRequest.customizeDescription('A key must be ' +
|
||||||
|
'specified'), null, response, 200, log);
|
||||||
|
}
|
||||||
|
api.callApiMethod('multipartDelete', request, response, log,
|
||||||
|
(err, corsHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
return routesUtils.responseNoBody(err, corsHeaders, response,
|
||||||
|
204, log);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (request.objectKey === undefined) {
|
||||||
|
if (request.query.website !== undefined) {
|
||||||
|
return api.callApiMethod('bucketDeleteWebsite', request,
|
||||||
|
response, log, (err, corsHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
return routesUtils.responseNoBody(err, corsHeaders,
|
||||||
|
response, 204, log);
|
||||||
|
});
|
||||||
|
} else if (request.query.cors !== undefined) {
|
||||||
|
return api.callApiMethod('bucketDeleteCors', request, response,
|
||||||
|
log, (err, corsHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
return routesUtils.responseNoBody(err, corsHeaders,
|
||||||
|
response, 204, log);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
api.callApiMethod('bucketDelete', request, response, log,
|
||||||
|
(err, corsHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
return routesUtils.responseNoBody(err, corsHeaders, response,
|
||||||
|
204, log);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (request.query.tagging !== undefined) {
|
||||||
|
return api.callApiMethod('objectDeleteTagging', request,
|
||||||
|
response, log, (err, resHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
return routesUtils.responseNoBody(err, resHeaders,
|
||||||
|
response, 204, log);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
api.callApiMethod('objectDelete', request, response, log,
|
||||||
|
(err, corsHeaders) => {
|
||||||
|
/*
|
||||||
|
* Since AWS expects a 204 regardless of the existence of
|
||||||
|
the object, the errors NoSuchKey and NoSuchVersion should not
|
||||||
|
* be sent back as a response.
|
||||||
|
*/
|
||||||
|
if (err && !err.NoSuchKey && !err.NoSuchVersion) {
|
||||||
|
return routesUtils.responseNoBody(err, corsHeaders,
|
||||||
|
response, null, log);
|
||||||
|
}
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
return routesUtils.responseNoBody(null, corsHeaders, response,
|
||||||
|
204, log);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = routeDELETE;
|
|
@ -0,0 +1,112 @@
|
||||||
|
const errors = require('../../errors');
|
||||||
|
const routesUtils = require('../routesUtils');
|
||||||
|
|
||||||
|
function routerGET(request, response, api, log, statsClient, dataRetrievalFn) {
|
||||||
|
log.debug('routing request', { method: 'routerGET' });
|
||||||
|
if (request.bucketName === undefined && request.objectKey !== undefined) {
|
||||||
|
routesUtils.responseXMLBody(errors.NoSuchBucket, null, response, log);
|
||||||
|
} else if (request.bucketName === undefined
|
||||||
|
&& request.objectKey === undefined) {
|
||||||
|
// GET service
|
||||||
|
api.callApiMethod('serviceGet', request, response, log, (err, xml) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
return routesUtils.responseXMLBody(err, xml, response, log);
|
||||||
|
});
|
||||||
|
} else if (request.objectKey === undefined) {
|
||||||
|
// GET bucket ACL
|
||||||
|
if (request.query.acl !== undefined) {
|
||||||
|
api.callApiMethod('bucketGetACL', request, response, log,
|
||||||
|
(err, xml, corsHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
return routesUtils.responseXMLBody(err, xml, response, log,
|
||||||
|
corsHeaders);
|
||||||
|
});
|
||||||
|
} else if (request.query.cors !== undefined) {
|
||||||
|
api.callApiMethod('bucketGetCors', request, response, log,
|
||||||
|
(err, xml, corsHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
routesUtils.responseXMLBody(err, xml, response, log,
|
||||||
|
corsHeaders);
|
||||||
|
});
|
||||||
|
} else if (request.query.versioning !== undefined) {
|
||||||
|
api.callApiMethod('bucketGetVersioning', request, response, log,
|
||||||
|
(err, xml, corsHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
routesUtils.responseXMLBody(err, xml, response, log,
|
||||||
|
corsHeaders);
|
||||||
|
});
|
||||||
|
} else if (request.query.website !== undefined) {
|
||||||
|
api.callApiMethod('bucketGetWebsite', request, response, log,
|
||||||
|
(err, xml, corsHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
routesUtils.responseXMLBody(err, xml, response, log,
|
||||||
|
corsHeaders);
|
||||||
|
});
|
||||||
|
} else if (request.query.uploads !== undefined) {
|
||||||
|
// List MultipartUploads
|
||||||
|
api.callApiMethod('listMultipartUploads', request, response, log,
|
||||||
|
(err, xml, corsHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
return routesUtils.responseXMLBody(err, xml, response, log,
|
||||||
|
corsHeaders);
|
||||||
|
});
|
||||||
|
} else if (request.query.location !== undefined) {
|
||||||
|
api.callApiMethod('bucketGetLocation', request, response, log,
|
||||||
|
(err, xml, corsHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
return routesUtils.responseXMLBody(err, xml, response, log,
|
||||||
|
corsHeaders);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// GET bucket
|
||||||
|
api.callApiMethod('bucketGet', request, response, log,
|
||||||
|
(err, xml, corsHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
return routesUtils.responseXMLBody(err, xml, response, log,
|
||||||
|
corsHeaders);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (request.query.acl !== undefined) {
|
||||||
|
// GET object ACL
|
||||||
|
api.callApiMethod('objectGetACL', request, response, log,
|
||||||
|
(err, xml, corsHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
return routesUtils.responseXMLBody(err, xml, response, log,
|
||||||
|
corsHeaders);
|
||||||
|
});
|
||||||
|
} else if (request.query.tagging !== undefined) {
|
||||||
|
// GET object Tagging
|
||||||
|
api.callApiMethod('objectGetTagging', request, response, log,
|
||||||
|
(err, xml, corsHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
return routesUtils.responseXMLBody(err, xml, response, log,
|
||||||
|
corsHeaders);
|
||||||
|
});
|
||||||
|
// List parts of an open multipart upload
|
||||||
|
} else if (request.query.uploadId !== undefined) {
|
||||||
|
api.callApiMethod('listParts', request, response, log,
|
||||||
|
(err, xml, corsHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
return routesUtils.responseXMLBody(err, xml, response, log,
|
||||||
|
corsHeaders);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// GET object
|
||||||
|
api.callApiMethod('objectGet', request, response, log,
|
||||||
|
(err, dataGetInfo, resMetaHeaders, range) => {
|
||||||
|
let contentLength = 0;
|
||||||
|
if (resMetaHeaders && resMetaHeaders['Content-Length']) {
|
||||||
|
contentLength = resMetaHeaders['Content-Length'];
|
||||||
|
}
|
||||||
|
log.end().addDefaultFields({ contentLength });
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
return routesUtils.responseStreamData(err, request.headers,
|
||||||
|
resMetaHeaders, dataGetInfo, dataRetrievalFn, response,
|
||||||
|
range, log);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = routerGET;
|
|
@ -0,0 +1,29 @@
|
||||||
|
const errors = require('../../errors');
|
||||||
|
const routesUtils = require('../routesUtils');
|
||||||
|
|
||||||
|
function routeHEAD(request, response, api, log, statsClient) {
|
||||||
|
log.debug('routing request', { method: 'routeHEAD' });
|
||||||
|
if (request.bucketName === undefined) {
|
||||||
|
log.trace('head request without bucketName');
|
||||||
|
routesUtils.responseXMLBody(errors.MethodNotAllowed,
|
||||||
|
null, response, log);
|
||||||
|
} else if (request.objectKey === undefined) {
|
||||||
|
// HEAD bucket
|
||||||
|
api.callApiMethod('bucketHead', request, response, log,
|
||||||
|
(err, corsHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
return routesUtils.responseNoBody(err, corsHeaders, response,
|
||||||
|
200, log);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// HEAD object
|
||||||
|
api.callApiMethod('objectHead', request, response, log,
|
||||||
|
(err, resHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
return routesUtils.responseContentHeaders(err, {}, resHeaders,
|
||||||
|
response, log);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = routeHEAD;
|
|
@ -0,0 +1,31 @@
|
||||||
|
const errors = require('../../errors');
|
||||||
|
const routesUtils = require('../routesUtils');
|
||||||
|
|
||||||
|
function routeOPTIONS(request, response, api, log, statsClient) {
|
||||||
|
log.debug('routing request', { method: 'routeOPTION' });
|
||||||
|
|
||||||
|
const corsMethod = request.headers['access-control-request-method'] || null;
|
||||||
|
|
||||||
|
if (!request.headers.origin) {
|
||||||
|
const msg = 'Insufficient information. Origin request header needed.';
|
||||||
|
const err = errors.BadRequest.customizeDescription(msg);
|
||||||
|
log.debug('missing origin', { method: 'routeOPTIONS', error: err });
|
||||||
|
return routesUtils.responseXMLBody(err, undefined, response, log);
|
||||||
|
}
|
||||||
|
if (['GET', 'PUT', 'HEAD', 'POST', 'DELETE'].indexOf(corsMethod) < 0) {
|
||||||
|
const msg = `Invalid Access-Control-Request-Method: ${corsMethod}`;
|
||||||
|
const err = errors.BadRequest.customizeDescription(msg);
|
||||||
|
log.debug('invalid Access-Control-Request-Method',
|
||||||
|
{ method: 'routeOPTIONS', error: err });
|
||||||
|
return routesUtils.responseXMLBody(err, undefined, response, log);
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.callApiMethod('corsPreflight', request, response, log,
|
||||||
|
(err, resHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
return routesUtils.responseNoBody(err, resHeaders, response, 200,
|
||||||
|
log);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = routeOPTIONS;
|
|
@ -0,0 +1,54 @@
|
||||||
|
const errors = require('../../errors');
|
||||||
|
const routesUtils = require('../routesUtils');
|
||||||
|
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
function routePOST(request, response, api, log) {
|
||||||
|
log.debug('routing request', { method: 'routePOST' });
|
||||||
|
|
||||||
|
const invalidMultiObjectDelReq = request.query.delete !== undefined
|
||||||
|
&& request.bucketName === undefined;
|
||||||
|
if (invalidMultiObjectDelReq) {
|
||||||
|
return routesUtils.responseNoBody(errors.MethodNotAllowed, null,
|
||||||
|
response, null, log);
|
||||||
|
}
|
||||||
|
|
||||||
|
request.post = '';
|
||||||
|
|
||||||
|
const invalidInitiateMpuReq = request.query.uploads !== undefined
|
||||||
|
&& request.objectKey === undefined;
|
||||||
|
const invalidCompleteMpuReq = request.query.uploadId !== undefined
|
||||||
|
&& request.objectKey === undefined;
|
||||||
|
if (invalidInitiateMpuReq || invalidCompleteMpuReq) {
|
||||||
|
return routesUtils.responseNoBody(errors.InvalidURI, null,
|
||||||
|
response, null, log);
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST initiate multipart upload
|
||||||
|
if (request.query.uploads !== undefined) {
|
||||||
|
return api.callApiMethod('initiateMultipartUpload', request,
|
||||||
|
response, log, (err, result, corsHeaders) =>
|
||||||
|
routesUtils.responseXMLBody(err, result, response, log,
|
||||||
|
corsHeaders));
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST complete multipart upload
|
||||||
|
if (request.query.uploadId !== undefined) {
|
||||||
|
return api.callApiMethod('completeMultipartUpload', request,
|
||||||
|
response, log, (err, result, resHeaders) =>
|
||||||
|
routesUtils.responseXMLBody(err, result, response, log,
|
||||||
|
resHeaders));
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST multiObjectDelete
|
||||||
|
if (request.query.delete !== undefined) {
|
||||||
|
return api.callApiMethod('multiObjectDelete', request, response,
|
||||||
|
log, (err, xml, corsHeaders) =>
|
||||||
|
routesUtils.responseXMLBody(err, xml, response, log,
|
||||||
|
corsHeaders));
|
||||||
|
}
|
||||||
|
|
||||||
|
return routesUtils.responseNoBody(errors.NotImplemented, null, response,
|
||||||
|
200, log);
|
||||||
|
}
|
||||||
|
/* eslint-enable no-param-reassign */
|
||||||
|
module.exports = routePOST;
|
|
@ -0,0 +1,182 @@
|
||||||
|
const errors = require('../../errors');
|
||||||
|
const routesUtils = require('../routesUtils');
|
||||||
|
|
||||||
|
const encryptionHeaders = [
|
||||||
|
'x-amz-server-side-encryption',
|
||||||
|
'x-amz-server-side-encryption-customer-algorithm',
|
||||||
|
'x-amz-server-side-encryption-aws-kms-key-id',
|
||||||
|
'x-amz-server-side-encryption-context',
|
||||||
|
'x-amz-server-side-encryption-customer-key',
|
||||||
|
'x-amz-server-side-encryption-customer-key-md5',
|
||||||
|
];
|
||||||
|
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
function routePUT(request, response, api, log, statsClient) {
|
||||||
|
log.debug('routing request', { method: 'routePUT' });
|
||||||
|
|
||||||
|
if (request.objectKey === undefined) {
|
||||||
|
// PUT bucket - PUT bucket ACL
|
||||||
|
|
||||||
|
// content-length for object is handled separately below
|
||||||
|
const contentLength = request.headers['content-length'];
|
||||||
|
if ((contentLength && (isNaN(contentLength) || contentLength < 0)) ||
|
||||||
|
contentLength === '') {
|
||||||
|
log.debug('invalid content-length header');
|
||||||
|
return routesUtils.responseNoBody(
|
||||||
|
errors.BadRequest, null, response, null, log);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT bucket ACL
|
||||||
|
if (request.query.acl !== undefined) {
|
||||||
|
api.callApiMethod('bucketPutACL', request, response, log,
|
||||||
|
(err, corsHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
return routesUtils.responseNoBody(err, corsHeaders,
|
||||||
|
response, 200, log);
|
||||||
|
});
|
||||||
|
} else if (request.query.versioning !== undefined) {
|
||||||
|
api.callApiMethod('bucketPutVersioning', request, response, log,
|
||||||
|
(err, corsHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
routesUtils.responseNoBody(err, corsHeaders, response, 200,
|
||||||
|
log);
|
||||||
|
});
|
||||||
|
} else if (request.query.website !== undefined) {
|
||||||
|
api.callApiMethod('bucketPutWebsite', request, response, log,
|
||||||
|
(err, corsHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
return routesUtils.responseNoBody(err, corsHeaders,
|
||||||
|
response, 200, log);
|
||||||
|
});
|
||||||
|
} else if (request.query.cors !== undefined) {
|
||||||
|
api.callApiMethod('bucketPutCors', request, response, log,
|
||||||
|
(err, corsHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
return routesUtils.responseNoBody(err, corsHeaders,
|
||||||
|
response, 200, log);
|
||||||
|
});
|
||||||
|
} else if (request.query.replication !== undefined) {
|
||||||
|
api.callApiMethod('bucketPutReplication', request, response, log,
|
||||||
|
(err, corsHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
routesUtils.responseNoBody(err, corsHeaders, response, 200,
|
||||||
|
log);
|
||||||
|
});
|
||||||
|
} else if (request.query.acl === undefined) {
|
||||||
|
// PUT bucket
|
||||||
|
return api.callApiMethod('bucketPut', request, response, log,
|
||||||
|
(err, corsHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
const location = { Location: `/${request.bucketName}` };
|
||||||
|
const resHeaders = corsHeaders ?
|
||||||
|
Object.assign({}, location, corsHeaders) : location;
|
||||||
|
return routesUtils.responseNoBody(err, resHeaders,
|
||||||
|
response, 200, log);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// PUT object, PUT object ACL, PUT object multipart or
|
||||||
|
// PUT object copy
|
||||||
|
// if content-md5 is not present in the headers, try to
|
||||||
|
// parse content-md5 from meta headers
|
||||||
|
|
||||||
|
if (request.headers['content-md5'] === '') {
|
||||||
|
log.debug('empty content-md5 header', {
|
||||||
|
method: 'routePUT',
|
||||||
|
});
|
||||||
|
return routesUtils
|
||||||
|
.responseNoBody(errors.InvalidDigest, null, response, 200, log);
|
||||||
|
}
|
||||||
|
if (request.headers['content-md5']) {
|
||||||
|
request.contentMD5 = request.headers['content-md5'];
|
||||||
|
} else {
|
||||||
|
request.contentMD5 = routesUtils.parseContentMD5(request.headers);
|
||||||
|
}
|
||||||
|
if (request.contentMD5 && request.contentMD5.length !== 32) {
|
||||||
|
request.contentMD5 = Buffer.from(request.contentMD5, 'base64')
|
||||||
|
.toString('hex');
|
||||||
|
if (request.contentMD5 && request.contentMD5.length !== 32) {
|
||||||
|
log.debug('invalid md5 digest', {
|
||||||
|
contentMD5: request.contentMD5,
|
||||||
|
});
|
||||||
|
return routesUtils
|
||||||
|
.responseNoBody(errors.InvalidDigest, null, response, 200,
|
||||||
|
log);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// object level encryption
|
||||||
|
if (encryptionHeaders.some(i => request.headers[i] !== undefined)) {
|
||||||
|
return routesUtils.responseXMLBody(errors.NotImplemented, null,
|
||||||
|
response, log);
|
||||||
|
}
|
||||||
|
if (request.query.partNumber) {
|
||||||
|
if (request.headers['x-amz-copy-source']) {
|
||||||
|
api.callApiMethod('objectPutCopyPart', request, response, log,
|
||||||
|
(err, xml, additionalHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
return routesUtils.responseXMLBody(err, xml, response, log,
|
||||||
|
additionalHeaders);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
api.callApiMethod('objectPutPart', request, response, log,
|
||||||
|
(err, calculatedHash, corsHeaders) => {
|
||||||
|
if (err) {
|
||||||
|
return routesUtils.responseNoBody(err, corsHeaders,
|
||||||
|
response, 200, log);
|
||||||
|
}
|
||||||
|
// ETag's hex should always be enclosed in quotes
|
||||||
|
const resMetaHeaders = corsHeaders || {};
|
||||||
|
resMetaHeaders.ETag = `"${calculatedHash}"`;
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
return routesUtils.responseNoBody(err, resMetaHeaders,
|
||||||
|
response, 200, log);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (request.query.acl !== undefined) {
|
||||||
|
api.callApiMethod('objectPutACL', request, response, log,
|
||||||
|
(err, resHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
return routesUtils.responseNoBody(err, resHeaders,
|
||||||
|
response, 200, log);
|
||||||
|
});
|
||||||
|
} else if (request.query.tagging !== undefined) {
|
||||||
|
api.callApiMethod('objectPutTagging', request, response, log,
|
||||||
|
(err, resHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
return routesUtils.responseNoBody(err, resHeaders,
|
||||||
|
response, 200, log);
|
||||||
|
});
|
||||||
|
} else if (request.headers['x-amz-copy-source']) {
|
||||||
|
return api.callApiMethod('objectCopy', request, response, log,
|
||||||
|
(err, xml, additionalHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
routesUtils.responseXMLBody(err, xml, response, log,
|
||||||
|
additionalHeaders);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (request.headers['content-length'] === undefined &&
|
||||||
|
request.headers['x-amz-decoded-content-length'] === undefined) {
|
||||||
|
return routesUtils.responseNoBody(errors.MissingContentLength,
|
||||||
|
null, response, 411, log);
|
||||||
|
}
|
||||||
|
if (Number.isNaN(request.parsedContentLength) ||
|
||||||
|
request.parsedContentLength < 0) {
|
||||||
|
return routesUtils.responseNoBody(errors.BadRequest,
|
||||||
|
null, response, 400, log);
|
||||||
|
}
|
||||||
|
log.end().addDefaultFields({
|
||||||
|
contentLength: request.parsedContentLength,
|
||||||
|
});
|
||||||
|
|
||||||
|
api.callApiMethod('objectPut', request, response, log,
|
||||||
|
(err, resHeaders) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
return routesUtils.responseNoBody(err, resHeaders,
|
||||||
|
response, 200, log);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
/* eslint-enable no-param-reassign */
|
||||||
|
module.exports = routePUT;
|
|
@ -0,0 +1,65 @@
|
||||||
|
const errors = require('../../errors');
|
||||||
|
const routesUtils = require('../routesUtils');
|
||||||
|
|
||||||
|
function routerWebsite(request, response, api, log, statsClient,
|
||||||
|
dataRetrievalFn) {
|
||||||
|
log.debug('routing request', { method: 'routerWebsite' });
|
||||||
|
// website endpoint only supports GET and HEAD and must have a bucket
|
||||||
|
// http://docs.aws.amazon.com/AmazonS3/latest/dev/WebsiteEndpoints.html
|
||||||
|
if ((request.method !== 'GET' && request.method !== 'HEAD')
|
||||||
|
|| !request.bucketName) {
|
||||||
|
return routesUtils.errorHtmlResponse(errors.MethodNotAllowed,
|
||||||
|
false, request.bucketName, response, null, log);
|
||||||
|
}
|
||||||
|
if (request.method === 'GET') {
|
||||||
|
return api.callApiMethod('websiteGet', request, response, log,
|
||||||
|
(err, userErrorPageFailure, dataGetInfo, resMetaHeaders,
|
||||||
|
redirectInfo, key) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
// request being redirected
|
||||||
|
if (redirectInfo) {
|
||||||
|
// note that key might have been modified in websiteGet
|
||||||
|
// api to add index document
|
||||||
|
return routesUtils.redirectRequest(redirectInfo,
|
||||||
|
key, request.connection.encrypted,
|
||||||
|
response, request.headers.host, resMetaHeaders, log);
|
||||||
|
}
|
||||||
|
// user has their own error page
|
||||||
|
if (err && dataGetInfo) {
|
||||||
|
return routesUtils.streamUserErrorPage(err, dataGetInfo,
|
||||||
|
dataRetrievalFn, response, resMetaHeaders, log);
|
||||||
|
}
|
||||||
|
// send default error html response
|
||||||
|
if (err) {
|
||||||
|
return routesUtils.errorHtmlResponse(err,
|
||||||
|
userErrorPageFailure, request.bucketName,
|
||||||
|
response, resMetaHeaders, log);
|
||||||
|
}
|
||||||
|
// no error, stream data
|
||||||
|
return routesUtils.responseStreamData(null, request.headers,
|
||||||
|
resMetaHeaders, dataGetInfo, dataRetrievalFn, response,
|
||||||
|
null, log);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (request.method === 'HEAD') {
|
||||||
|
return api.callApiMethod('websiteHead', request, response, log,
|
||||||
|
(err, resMetaHeaders, redirectInfo, key) => {
|
||||||
|
routesUtils.statsReport500(err, statsClient);
|
||||||
|
if (redirectInfo) {
|
||||||
|
return routesUtils.redirectRequest(redirectInfo,
|
||||||
|
key, request.connection.encrypted,
|
||||||
|
response, request.headers.host, resMetaHeaders, log);
|
||||||
|
}
|
||||||
|
// could redirect on err so check for redirectInfo first
|
||||||
|
if (err) {
|
||||||
|
return routesUtils.errorHeaderResponse(err, response,
|
||||||
|
resMetaHeaders, log);
|
||||||
|
}
|
||||||
|
return routesUtils.responseContentHeaders(err, {}, resMetaHeaders,
|
||||||
|
response, log);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = routerWebsite;
|
|
@ -0,0 +1,809 @@
|
||||||
|
const url = require('url');
|
||||||
|
const ipCheck = require('../ipCheck');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* setCommonResponseHeaders - Set HTTP response headers
|
||||||
|
* @param {object} headers - key and value of new headers to add
|
||||||
|
* @param {object} response - http response object
|
||||||
|
* @param {object} log - Werelogs logger
|
||||||
|
* @return {object} response - response object with additional headers
|
||||||
|
*/
|
||||||
|
function setCommonResponseHeaders(headers, response, log) {
|
||||||
|
if (headers && typeof headers === 'object') {
|
||||||
|
log.trace('setting response headers', { headers });
|
||||||
|
Object.keys(headers).forEach(key => {
|
||||||
|
if (headers[key] !== undefined) {
|
||||||
|
try {
|
||||||
|
response.setHeader(key, headers[key]);
|
||||||
|
} catch (e) {
|
||||||
|
log.debug('header can not be added ' +
|
||||||
|
'to the response', { header: headers[key],
|
||||||
|
error: e.stack, method: 'setCommonResponseHeaders' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
response.setHeader('server', 'S3 Server');
|
||||||
|
// to be expanded in further implementation of logging of requests
|
||||||
|
response.setHeader('x-amz-id-2', log.getSerializedUids());
|
||||||
|
response.setHeader('x-amz-request-id', log.getSerializedUids());
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* okHeaderResponse - Response with only headers, no body
|
||||||
|
* @param {object} headers - key and value of new headers to add
|
||||||
|
* @param {object} response - http response object
|
||||||
|
* @param {number} httpCode -- http response code
|
||||||
|
* @param {object} log - Werelogs logger
|
||||||
|
* @return {object} response - response object with additional headers
|
||||||
|
*/
|
||||||
|
function okHeaderResponse(headers, response, httpCode, log) {
|
||||||
|
log.trace('sending success header response');
|
||||||
|
setCommonResponseHeaders(headers, response, log);
|
||||||
|
log.debug('response http code', { httpCode });
|
||||||
|
response.writeHead(httpCode);
|
||||||
|
return response.end(() => {
|
||||||
|
log.end().info('responded to request', {
|
||||||
|
httpCode: response.statusCode,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* okXMLResponse - Response with XML body
|
||||||
|
* @param {string} xml - XML body as string
|
||||||
|
* @param {object} response - http response object
|
||||||
|
* @param {object} log - Werelogs logger
|
||||||
|
* @param {object} additionalHeaders -- additional headers to add to response
|
||||||
|
* @return {object} response - response object with additional headers
|
||||||
|
*/
|
||||||
|
function okXMLResponse(xml, response, log, additionalHeaders) {
|
||||||
|
const bytesSent = Buffer.byteLength(xml);
|
||||||
|
log.trace('sending success xml response');
|
||||||
|
log.addDefaultFields({
|
||||||
|
bytesSent,
|
||||||
|
});
|
||||||
|
setCommonResponseHeaders(additionalHeaders, response, log);
|
||||||
|
response.writeHead(200, { 'Content-type': 'application/xml' });
|
||||||
|
log.debug('response http code', { httpCode: 200 });
|
||||||
|
log.trace('xml response', { xml });
|
||||||
|
return response.end(xml, 'utf8', () => {
|
||||||
|
log.end().info('responded with XML', {
|
||||||
|
httpCode: response.statusCode,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorXMLResponse(errCode, response, log, corsHeaders) {
|
||||||
|
log.trace('sending error xml response', { errCode });
|
||||||
|
/*
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Error>
|
||||||
|
<Code>NoSuchKey</Code>
|
||||||
|
<Message>The resource you requested does not exist</Message>
|
||||||
|
<Resource>/mybucket/myfoto.jpg</Resource>
|
||||||
|
<RequestId>4442587FB7D0A2F9</RequestId>
|
||||||
|
</Error>
|
||||||
|
*/
|
||||||
|
const xml = [];
|
||||||
|
xml.push(
|
||||||
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||||
|
'<Error>',
|
||||||
|
`<Code>${errCode.message}</Code>`,
|
||||||
|
`<Message>${errCode.description}</Message>`,
|
||||||
|
'<Resource></Resource>',
|
||||||
|
`<RequestId>${log.getSerializedUids()}</RequestId>`,
|
||||||
|
'</Error>'
|
||||||
|
);
|
||||||
|
const xmlStr = xml.join('');
|
||||||
|
const bytesSent = Buffer.byteLength(xmlStr);
|
||||||
|
log.addDefaultFields({
|
||||||
|
bytesSent,
|
||||||
|
});
|
||||||
|
if (corsHeaders) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
corsHeaders['Content-Type'] = 'application/xml';
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
corsHeaders['Content-Length'] = xmlStr.length;
|
||||||
|
}
|
||||||
|
setCommonResponseHeaders(corsHeaders, response, log);
|
||||||
|
response.writeHead(errCode.code, { 'Content-type': 'application/xml' });
|
||||||
|
return response.end(xmlStr, 'utf8', () => {
|
||||||
|
log.end().info('responded with error XML', {
|
||||||
|
httpCode: response.statusCode,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modify response headers for an objectGet or objectHead request
|
||||||
|
* @param {object} overrideHeaders - headers in this object override common
|
||||||
|
* headers. These are extracted from the request object
|
||||||
|
* @param {object} resHeaders - object with common response headers
|
||||||
|
* @param {object} response - router's response object
|
||||||
|
* @param {array | undefined} range - range in form of [start, end]
|
||||||
|
* or undefined if no range header
|
||||||
|
* @param {object} log - Werelogs logger
|
||||||
|
* @return {object} response - modified response object
|
||||||
|
*/
|
||||||
|
function okContentHeadersResponse(overrideHeaders, resHeaders,
|
||||||
|
response, range, log) {
|
||||||
|
const addHeaders = {};
|
||||||
|
if (process.env.ALLOW_INVALID_META_HEADERS) {
|
||||||
|
const headersArr = Object.keys(resHeaders);
|
||||||
|
const length = headersArr.length;
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
const headerName = headersArr[i];
|
||||||
|
if (headerName.startsWith('x-amz-')) {
|
||||||
|
const translatedHeaderName = headerName.replace(/\//g, '|+2f');
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
resHeaders[translatedHeaderName] =
|
||||||
|
resHeaders[headerName];
|
||||||
|
if (translatedHeaderName !== headerName) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
delete resHeaders[headerName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(addHeaders, resHeaders);
|
||||||
|
|
||||||
|
if (overrideHeaders['response-content-type']) {
|
||||||
|
addHeaders['Content-Type'] = overrideHeaders['response-content-type'];
|
||||||
|
}
|
||||||
|
if (overrideHeaders['response-content-language']) {
|
||||||
|
addHeaders['Content-Language'] =
|
||||||
|
overrideHeaders['response-content-language'];
|
||||||
|
}
|
||||||
|
if (overrideHeaders['response-expires']) {
|
||||||
|
addHeaders.Expires = overrideHeaders['response-expires'];
|
||||||
|
}
|
||||||
|
if (overrideHeaders['response-cache-control']) {
|
||||||
|
addHeaders['Cache-Control'] = overrideHeaders['response-cache-control'];
|
||||||
|
}
|
||||||
|
if (overrideHeaders['response-content-disposition']) {
|
||||||
|
addHeaders['Content-Disposition'] =
|
||||||
|
overrideHeaders['response-content-disposition'];
|
||||||
|
}
|
||||||
|
if (overrideHeaders['response-content-encoding']) {
|
||||||
|
addHeaders['Content-Encoding'] =
|
||||||
|
overrideHeaders['response-content-encoding'];
|
||||||
|
}
|
||||||
|
setCommonResponseHeaders(addHeaders, response, log);
|
||||||
|
const httpCode = range ? 206 : 200;
|
||||||
|
log.debug('response http code', { httpCode });
|
||||||
|
response.writeHead(httpCode);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
function retrieveData(locations, dataRetrievalFn,
|
||||||
|
response, logger, errorHandlerFn) {
|
||||||
|
if (locations.length === 0) {
|
||||||
|
return response.end();
|
||||||
|
}
|
||||||
|
if (errorHandlerFn === undefined) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
errorHandlerFn = () => { response.connection.destroy(); };
|
||||||
|
}
|
||||||
|
const current = locations.shift();
|
||||||
|
return dataRetrievalFn(current, logger,
|
||||||
|
(err, readable) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error('failed to get object', {
|
||||||
|
error: err,
|
||||||
|
method: 'retrieveData',
|
||||||
|
});
|
||||||
|
return errorHandlerFn(err);
|
||||||
|
}
|
||||||
|
readable.on('error', err => {
|
||||||
|
logger.error('error piping data from source');
|
||||||
|
errorHandlerFn(err);
|
||||||
|
});
|
||||||
|
readable.on('end', () => {
|
||||||
|
process.nextTick(retrieveData,
|
||||||
|
locations, dataRetrievalFn, response, logger);
|
||||||
|
});
|
||||||
|
readable.pipe(response, { end: false });
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const routesUtils = {
|
||||||
|
/**
|
||||||
|
* @param {string} errCode - S3 error Code
|
||||||
|
* @param {string} xml - xml body as string conforming to S3's spec.
|
||||||
|
* @param {object} response - router's response object
|
||||||
|
* @param {object} log - Werelogs logger
|
||||||
|
* @param {object} [additionalHeaders] - additionalHeaders to add
|
||||||
|
* to response
|
||||||
|
* @return {function} - error or success response utility
|
||||||
|
*/
|
||||||
|
responseXMLBody(errCode, xml, response, log, additionalHeaders) {
|
||||||
|
if (errCode && !response.headersSent) {
|
||||||
|
return errorXMLResponse(errCode, response, log, additionalHeaders);
|
||||||
|
}
|
||||||
|
if (!response.headersSent) {
|
||||||
|
return okXMLResponse(xml, response, log, additionalHeaders);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} errCode - S3 error Code
|
||||||
|
* @param {string} resHeaders - headers to be set for the response
|
||||||
|
* @param {object} response - router's response object
|
||||||
|
* @param {number} httpCode - httpCode to set in response
|
||||||
|
* If none provided, defaults to 200.
|
||||||
|
* @param {object} log - Werelogs logger
|
||||||
|
* @return {function} - error or success response utility
|
||||||
|
*/
|
||||||
|
responseNoBody(errCode, resHeaders, response, httpCode = 200, log) {
|
||||||
|
if (errCode && !response.headersSent) {
|
||||||
|
return errorXMLResponse(errCode, response, log, resHeaders);
|
||||||
|
}
|
||||||
|
if (!response.headersSent) {
|
||||||
|
return okHeaderResponse(resHeaders, response, httpCode, log);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} errCode - S3 error Code
|
||||||
|
* @param {object} overrideHeaders - headers in this object override common
|
||||||
|
* headers. These are extracted from the request object
|
||||||
|
* @param {string} resHeaders - headers to be set for the response
|
||||||
|
* @param {object} response - router's response object
|
||||||
|
* @param {object} log - Werelogs logger
|
||||||
|
* @return {object} - router's response object
|
||||||
|
*/
|
||||||
|
responseContentHeaders(errCode, overrideHeaders, resHeaders, response,
|
||||||
|
log) {
|
||||||
|
if (errCode && !response.headersSent) {
|
||||||
|
return errorXMLResponse(errCode, response, log, resHeaders);
|
||||||
|
}
|
||||||
|
if (!response.headersSent) {
|
||||||
|
// Undefined added as an argument since need to send range to
|
||||||
|
// okContentHeadersResponse in responseStreamData
|
||||||
|
okContentHeadersResponse(overrideHeaders, resHeaders, response,
|
||||||
|
undefined, log);
|
||||||
|
}
|
||||||
|
return response.end(() => {
|
||||||
|
log.end().info('responded with content headers', {
|
||||||
|
httpCode: response.statusCode,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {array} dataLocations - all data locations
|
||||||
|
* @param {array} outerRange - range from request
|
||||||
|
* @return {array} parsedLocations - dataLocations filtered for
|
||||||
|
* what needed and ranges added for particular parts as needed
|
||||||
|
*/
|
||||||
|
setPartRanges(dataLocations, outerRange) {
|
||||||
|
const parsedLocations = [];
|
||||||
|
const begin = outerRange[0];
|
||||||
|
const end = outerRange[1];
|
||||||
|
// If have single location, do not need to break up range among parts
|
||||||
|
// and might not have a start and size property
|
||||||
|
// on the dataLocation (because might be pre- md-model-version 2),
|
||||||
|
// so just set range as property
|
||||||
|
if (dataLocations.length === 1) {
|
||||||
|
const soleLocation = dataLocations[0];
|
||||||
|
soleLocation.range = [begin, end];
|
||||||
|
// If missing size, does not impact get range.
|
||||||
|
// We modify size here in case this function is used for
|
||||||
|
// object put part copy where will need size.
|
||||||
|
// If pre-md-model-version 2, object put part copy will not
|
||||||
|
// be allowed, so not an issue that size not modified here.
|
||||||
|
if (dataLocations[0].size) {
|
||||||
|
const partSize = parseInt(dataLocations[0].size, 10);
|
||||||
|
soleLocation.size =
|
||||||
|
Math.min(partSize, end - begin + 1).toString();
|
||||||
|
}
|
||||||
|
parsedLocations.push(soleLocation);
|
||||||
|
return parsedLocations;
|
||||||
|
}
|
||||||
|
// Range is inclusive of endpoint so need plus 1
|
||||||
|
const max = end - begin + 1;
|
||||||
|
let total = 0;
|
||||||
|
for (let i = 0; i < dataLocations.length; i++) {
|
||||||
|
if (total >= max) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const partStart = parseInt(dataLocations[i].start, 10);
|
||||||
|
const partSize = parseInt(dataLocations[i].size, 10);
|
||||||
|
if (partStart + partSize <= begin) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (partStart >= begin) {
|
||||||
|
// If the whole part is in the range, just include it
|
||||||
|
if (partSize + total <= max) {
|
||||||
|
const partWithoutRange = dataLocations[i];
|
||||||
|
partWithoutRange.size = partSize.toString();
|
||||||
|
parsedLocations.push(partWithoutRange);
|
||||||
|
total += partSize;
|
||||||
|
// Otherwise set a range limit on the part end
|
||||||
|
// and we're done
|
||||||
|
} else {
|
||||||
|
const partWithRange = dataLocations[i];
|
||||||
|
// Need to subtract one from endPart since range
|
||||||
|
// includes endPart in byte count
|
||||||
|
const endPart = Math.min(partSize - 1, max - total - 1);
|
||||||
|
partWithRange.range = [0, endPart];
|
||||||
|
// modify size to be stored for object put part copy
|
||||||
|
partWithRange.size = (endPart + 1).toString();
|
||||||
|
parsedLocations.push(dataLocations[i]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Offset start (and end if necessary)
|
||||||
|
const partWithRange = dataLocations[i];
|
||||||
|
const startOffset = begin - partStart;
|
||||||
|
// Use full remaining part if remaining partSize is less
|
||||||
|
// than byte range we need to satisfy. Or use byte range
|
||||||
|
// we need to satisfy taking into account any startOffset
|
||||||
|
const endPart = Math.min(partSize - 1,
|
||||||
|
max - total + startOffset - 1);
|
||||||
|
partWithRange.range = [startOffset, endPart];
|
||||||
|
// modify size to be stored for object put part copy
|
||||||
|
partWithRange.size = (endPart - startOffset + 1).toString();
|
||||||
|
parsedLocations.push(partWithRange);
|
||||||
|
// Need to add byte back since with total we are counting
|
||||||
|
// number of bytes while the endPart and startOffset
|
||||||
|
// are in terms of range which include the endpoint
|
||||||
|
total += endPart - startOffset + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parsedLocations;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} errCode - S3 error Code
|
||||||
|
* @param {object} overrideHeaders - headers in this object override common
|
||||||
|
* headers. These are extracted from the request object
|
||||||
|
* @param {string} resHeaders - headers to be set for the response
|
||||||
|
* @param {array | null} dataLocations --
|
||||||
|
* - array of locations to get streams from sproxyd
|
||||||
|
* - null if no data for object and only metadata
|
||||||
|
* @param {function} dataRetrievalFn - function to handle streaming data
|
||||||
|
* @param {http.ServerResponse} response - response sent to the client
|
||||||
|
* @param {array | undefined} range - range in format of [start, end]
|
||||||
|
* if range header contained in request or undefined if not
|
||||||
|
* @param {object} log - Werelogs logger
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
responseStreamData(errCode, overrideHeaders, resHeaders, dataLocations,
|
||||||
|
dataRetrievalFn, response, range, log) {
|
||||||
|
if (errCode && !response.headersSent) {
|
||||||
|
return errorXMLResponse(errCode, response, log, resHeaders);
|
||||||
|
}
|
||||||
|
if (!response.headersSent) {
|
||||||
|
okContentHeadersResponse(overrideHeaders, resHeaders, response,
|
||||||
|
range, log);
|
||||||
|
}
|
||||||
|
if (dataLocations === null) {
|
||||||
|
return response.end(() => {
|
||||||
|
log.end().info('responded with only metadata', {
|
||||||
|
httpCode: response.statusCode,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// TODO: may need to extract this logic to make this streaming method
|
||||||
|
// general enough for azure
|
||||||
|
const parsedLocations = range ? routesUtils
|
||||||
|
.setPartRanges(dataLocations, range) : dataLocations.slice();
|
||||||
|
response.on('finish', () => {
|
||||||
|
log.end().info('responded with streamed content', {
|
||||||
|
httpCode: response.statusCode,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return retrieveData(parsedLocations, dataRetrievalFn, response, log);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} err -- arsenal error object
|
||||||
|
* @param {array} dataLocations --
|
||||||
|
* - array of locations to get streams from backend
|
||||||
|
* @param {function} dataRetrievalFn - function to handle streaming data
|
||||||
|
* @param {http.ServerResponse} response - response sent to the client
|
||||||
|
* @param {object} corsHeaders - CORS-related response headers
|
||||||
|
* @param {object} log - Werelogs logger
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
streamUserErrorPage(err, dataLocations, dataRetrievalFn, response,
|
||||||
|
corsHeaders, log) {
|
||||||
|
setCommonResponseHeaders(corsHeaders, response, log);
|
||||||
|
response.writeHead(err.code, { 'Content-type': 'text/html' });
|
||||||
|
response.on('finish', () => {
|
||||||
|
log.end().info('responded with streamed content', {
|
||||||
|
httpCode: response.statusCode,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return retrieveData(dataLocations, dataRetrievalFn, response, log);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} err - arsenal error object
|
||||||
|
* @param {boolean} userErrorPageFailure - whether there was a failure
|
||||||
|
* retrieving the user's error page
|
||||||
|
* @param {string} bucketName - bucketName from request
|
||||||
|
* @param {http.ServerResponse} response - response sent to the client
|
||||||
|
* @param {object} corsHeaders - CORS-related response headers
|
||||||
|
* @param {object} log - Werelogs logger
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
errorHtmlResponse(err, userErrorPageFailure, bucketName, response,
|
||||||
|
corsHeaders, log) {
|
||||||
|
log.trace('sending generic html error page',
|
||||||
|
{ err });
|
||||||
|
setCommonResponseHeaders(corsHeaders, response, log);
|
||||||
|
response.writeHead(err.code, { 'Content-type': 'text/html' });
|
||||||
|
const html = [];
|
||||||
|
// response.statusMessage will provide standard message for status
|
||||||
|
// code so much set response status code before creating html
|
||||||
|
html.push(
|
||||||
|
'<html>',
|
||||||
|
'<head>',
|
||||||
|
`<title>${err.code} ${response.statusMessage}</title>`,
|
||||||
|
'</head>',
|
||||||
|
'<body>',
|
||||||
|
`<h1>${err.code} ${response.statusMessage}</h1>`,
|
||||||
|
'<ul>',
|
||||||
|
`<li>Code: ${err.message}</li>`,
|
||||||
|
`<li>Message: ${err.description}</li>`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!userErrorPageFailure && bucketName) {
|
||||||
|
html.push(`<li>BucketName: ${bucketName}</li>`);
|
||||||
|
}
|
||||||
|
html.push(
|
||||||
|
`<li>RequestId: ${log.getSerializedUids()}</li>`,
|
||||||
|
// AWS response contains HostId here.
|
||||||
|
// TODO: consider adding
|
||||||
|
'</ul>'
|
||||||
|
);
|
||||||
|
if (userErrorPageFailure) {
|
||||||
|
html.push(
|
||||||
|
'<h3>An Error Occurred While Attempting ',
|
||||||
|
'to Retrieve a Custom ',
|
||||||
|
'Error Document</h3>',
|
||||||
|
'<ul>',
|
||||||
|
`<li>Code: ${err.message}</li>`,
|
||||||
|
`<li>Message: ${err.description}</li>`,
|
||||||
|
'</ul>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
html.push(
|
||||||
|
'<hr/>',
|
||||||
|
'</body>',
|
||||||
|
'</html>'
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.end(html.join(''), 'utf8', () => {
|
||||||
|
log.end().info('responded with error html', {
|
||||||
|
httpCode: response.statusCode,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} err - arsenal error object
|
||||||
|
* @param {http.ServerResponse} response - response sent to the client
|
||||||
|
* @param {object} corsHeaders - CORS-related response headers
|
||||||
|
* @param {object} log - Werelogs logger
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
errorHeaderResponse(err, response, corsHeaders, log) {
|
||||||
|
log.trace('sending error header response',
|
||||||
|
{ err });
|
||||||
|
setCommonResponseHeaders(corsHeaders, response, log);
|
||||||
|
response.setHeader('x-amz-error-code', err.message);
|
||||||
|
response.setHeader('x-amz-error-message', err.description);
|
||||||
|
response.writeHead(err.code);
|
||||||
|
return response.end(() => {
|
||||||
|
log.end().info('responded with error headers', {
|
||||||
|
httpCode: response.statusCode,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get bucket name and object name from the request
|
||||||
|
* @param {object} request - http request object
|
||||||
|
* @param {string} pathname - http request path parsed from request url
|
||||||
|
* @param {string[]} validHosts - all region endpoints + websiteEndpoints
|
||||||
|
* @returns {object} result - returns object containing bucket
|
||||||
|
* name and objectKey as key
|
||||||
|
*/
|
||||||
|
getResourceNames(request, pathname, validHosts) {
|
||||||
|
return this.getNamesFromReq(request, pathname,
|
||||||
|
routesUtils.getBucketNameFromHost(request, validHosts));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get bucket name and/or object name from the path of a request
|
||||||
|
* @param {object} request - http request object
|
||||||
|
* @param {string} pathname - http request path parsed from request url
|
||||||
|
* @param {string} bucketNameFromHost - name of bucket from host name
|
||||||
|
* @returns {object} resources - returns object w. bucket and object as keys
|
||||||
|
*/
|
||||||
|
getNamesFromReq(request, pathname,
|
||||||
|
bucketNameFromHost) {
|
||||||
|
const resources = {
|
||||||
|
bucket: undefined,
|
||||||
|
object: undefined,
|
||||||
|
host: undefined,
|
||||||
|
gotBucketNameFromHost: undefined,
|
||||||
|
path: undefined,
|
||||||
|
};
|
||||||
|
// If there are spaces in a key name, s3cmd sends them as "+"s.
|
||||||
|
// Actual "+"s are uri encoded as "%2B" so by switching "+"s to
|
||||||
|
// spaces here, you still retain any "+"s in the final decoded path
|
||||||
|
const pathWithSpacesInsteadOfPluses = pathname.replace(/\+/g, ' ');
|
||||||
|
const path = decodeURIComponent(pathWithSpacesInsteadOfPluses);
|
||||||
|
resources.path = path;
|
||||||
|
|
||||||
|
let fullHost;
|
||||||
|
if (request.headers && request.headers.host) {
|
||||||
|
const reqHost = request.headers.host;
|
||||||
|
const bracketIndex = reqHost.indexOf(']');
|
||||||
|
const colonIndex = reqHost.lastIndexOf(':');
|
||||||
|
const hostLength = colonIndex > bracketIndex ?
|
||||||
|
colonIndex : reqHost.length;
|
||||||
|
fullHost = reqHost.slice(0, hostLength);
|
||||||
|
} else {
|
||||||
|
fullHost = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bucketNameFromHost) {
|
||||||
|
resources.bucket = bucketNameFromHost;
|
||||||
|
const bucketNameLength = bucketNameFromHost.length;
|
||||||
|
resources.host = fullHost.slice(bucketNameLength + 1);
|
||||||
|
// Slice off leading '/'
|
||||||
|
resources.object = path.slice(1);
|
||||||
|
resources.gotBucketNameFromHost = true;
|
||||||
|
} else {
|
||||||
|
resources.host = fullHost;
|
||||||
|
const urlArr = path.split('/');
|
||||||
|
if (urlArr.length > 1) {
|
||||||
|
resources.bucket = urlArr[1];
|
||||||
|
resources.object = urlArr.slice(2).join('/');
|
||||||
|
} else if (urlArr.length === 1) {
|
||||||
|
resources.bucket = urlArr[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// remove any empty strings or nulls
|
||||||
|
if (resources.bucket === '' || resources.bucket === null) {
|
||||||
|
resources.bucket = undefined;
|
||||||
|
}
|
||||||
|
if (resources.object === '' || resources.object === null) {
|
||||||
|
resources.object = undefined;
|
||||||
|
}
|
||||||
|
return resources;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get bucket name from the request of a virtually hosted bucket
|
||||||
|
* @param {object} request - HTTP request object
|
||||||
|
* @return {string|undefined} - returns bucket name if dns-style query
|
||||||
|
* returns undefined if path-style query
|
||||||
|
* @param {string[]} validHosts - all region endpoints + websiteEndpoints
|
||||||
|
* @throws {Error} in case the type of query could not be infered
|
||||||
|
*/
|
||||||
|
getBucketNameFromHost(request, validHosts) {
|
||||||
|
const headers = request.headers;
|
||||||
|
if (headers === undefined || headers.host === undefined) {
|
||||||
|
throw new Error('bad request: no host in headers');
|
||||||
|
}
|
||||||
|
const reqHost = headers.host;
|
||||||
|
const bracketIndex = reqHost.indexOf(']');
|
||||||
|
const colonIndex = reqHost.lastIndexOf(':');
|
||||||
|
|
||||||
|
const hostLength = colonIndex > bracketIndex ?
|
||||||
|
colonIndex : reqHost.length;
|
||||||
|
// If request is made using IPv6 (indicated by presence of brackets),
|
||||||
|
// surrounding brackets should not be included in host var
|
||||||
|
const host = bracketIndex > -1 ?
|
||||||
|
reqHost.slice(1, hostLength - 1) : reqHost.slice(0, hostLength);
|
||||||
|
// parseIp returns empty object if host is not valid IP
|
||||||
|
// If host is an IP address, it's path-style
|
||||||
|
if (Object.keys(ipCheck.parseIp(host)).length !== 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let bucketName;
|
||||||
|
for (let i = 0; i < validHosts.length; ++i) {
|
||||||
|
if (host === validHosts[i]) {
|
||||||
|
// It's path-style
|
||||||
|
return undefined;
|
||||||
|
} else if (host.endsWith(`.${validHosts[i]}`)) {
|
||||||
|
const potentialBucketName = host.split(`.${validHosts[i]}`)[0];
|
||||||
|
if (!bucketName) {
|
||||||
|
bucketName = potentialBucketName;
|
||||||
|
} else {
|
||||||
|
// bucketName should be shortest so that takes into account
|
||||||
|
// most specific potential hostname
|
||||||
|
bucketName =
|
||||||
|
potentialBucketName.length < bucketName.length ?
|
||||||
|
potentialBucketName : bucketName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bucketName) {
|
||||||
|
return bucketName;
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`bad request: hostname ${host} is not in valid endpoints`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modify http request object
|
||||||
|
* @param {object} request - http request object
|
||||||
|
* @param {string[]} validHosts - all region endpoints + websiteEndpoints
|
||||||
|
* @return {object} request object with additional attributes
|
||||||
|
*/
|
||||||
|
normalizeRequest(request, validHosts) {
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
const parsedUrl = url.parse(request.url, true);
|
||||||
|
request.query = parsedUrl.query;
|
||||||
|
// TODO: make the namespace come from a config variable.
|
||||||
|
request.namespace = 'default';
|
||||||
|
// Parse bucket and/or object names from request
|
||||||
|
const resources = this.getResourceNames(request, parsedUrl.pathname,
|
||||||
|
validHosts);
|
||||||
|
request.gotBucketNameFromHost = resources.gotBucketNameFromHost;
|
||||||
|
request.bucketName = resources.bucket;
|
||||||
|
request.objectKey = resources.object;
|
||||||
|
request.parsedHost = resources.host;
|
||||||
|
request.path = resources.path;
|
||||||
|
// For streaming v4 auth, the total body content length
|
||||||
|
// without the chunk metadata is sent as
|
||||||
|
// the x-amz-decoded-content-length
|
||||||
|
const contentLength = request.headers['x-amz-decoded-content-length'] ?
|
||||||
|
request.headers['x-amz-decoded-content-length'] :
|
||||||
|
request.headers['content-length'];
|
||||||
|
request.parsedContentLength =
|
||||||
|
Number.parseInt(contentLength, 10);
|
||||||
|
|
||||||
|
if (process.env.ALLOW_INVALID_META_HEADERS) {
|
||||||
|
const headersArr = Object.keys(request.headers);
|
||||||
|
const length = headersArr.length;
|
||||||
|
if (headersArr.indexOf('x-invalid-metadata') > 1) {
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
const headerName = headersArr[i];
|
||||||
|
if (headerName.startsWith('x-amz-')) {
|
||||||
|
const translatedHeaderName =
|
||||||
|
headerName.replace(/\|\+2f/g, '/');
|
||||||
|
request.headers[translatedHeaderName] =
|
||||||
|
request.headers[headerName];
|
||||||
|
if (translatedHeaderName !== headerName) {
|
||||||
|
delete request.headers[headerName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return request;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse content-md5 from meta headers
|
||||||
|
* @param {string} headers - request headers
|
||||||
|
* @return {string} - returns content-md5 string
|
||||||
|
*/
|
||||||
|
parseContentMD5(headers) {
|
||||||
|
if (headers['x-amz-meta-s3cmd-attrs']) {
|
||||||
|
const metaHeadersArr = headers['x-amz-meta-s3cmd-attrs'].split('/');
|
||||||
|
for (let i = 0; i < metaHeadersArr.length; i++) {
|
||||||
|
const tmpArr = metaHeadersArr[i].split(':');
|
||||||
|
if (tmpArr[0] === 'md5') {
|
||||||
|
return tmpArr[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if request query contains unsupported query
|
||||||
|
* @param {object} queryObj - parsed http request query
|
||||||
|
* @param {object} unsupportedQueries - true/false value for whether queries
|
||||||
|
* are supported
|
||||||
|
* @return {boolean} true/false - whether query contains unsupported query
|
||||||
|
*/
|
||||||
|
isUnsupportedQuery(queryObj, unsupportedQueries) {
|
||||||
|
return Object.keys(queryObj)
|
||||||
|
.some(key => unsupportedQueries[key]);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report 500 to stats when an Internal Error occurs
|
||||||
|
* @param {object} err - Arsenal error
|
||||||
|
* @param {object} statsClient - StatsClient instance
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
statsReport500(err, statsClient) {
|
||||||
|
if (statsClient && err && err.code === 500) {
|
||||||
|
statsClient.report500();
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* redirectRequest - redirectRequest based on rule
|
||||||
|
* @param {object} routingInfo - info for routing
|
||||||
|
* @param {string} [routingInfo.hostName] - redirect host
|
||||||
|
* @param {string} [routingInfo.protocol] - protocol for redirect
|
||||||
|
* (http or https)
|
||||||
|
* @param {number} [routingInfo.httpRedirectCode] - redirect http code
|
||||||
|
* @param {string} [routingInfo.replaceKeyPrefixWith] - repalcement prefix
|
||||||
|
* @param {string} [routingInfo.replaceKeyWith] - replacement key
|
||||||
|
* @param {string} [routingInfo.prefixFromRule] - key prefix to be replaced
|
||||||
|
* @param {boolean} [routingInfo.justPath] - whether to just send the
|
||||||
|
* path as the redirect location header rather than full protocol plus
|
||||||
|
* hostname plus path (AWS only sends path when redirect is based on
|
||||||
|
* x-amz-website-redirect-location header and redirect is to key in
|
||||||
|
* same bucket)
|
||||||
|
* @param {boolean} [routingInfo.redirectLocationHeader] - whether redirect
|
||||||
|
* rule came from an x-amz-website-redirect-location header
|
||||||
|
* @param {string} objectKey - key name (may have been modified in
|
||||||
|
* websiteGet api to include index document)
|
||||||
|
* @param {boolean} encrypted - whether request was https
|
||||||
|
* @param {object} response - response object
|
||||||
|
* @param {string} hostHeader - host sent in original request.headers
|
||||||
|
* @param {object} corsHeaders - CORS-related response headers
|
||||||
|
* @param {object} log - Werelogs instance
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
redirectRequest(routingInfo, objectKey, encrypted, response, hostHeader,
|
||||||
|
corsHeaders, log) {
|
||||||
|
const { justPath, redirectLocationHeader, hostName, protocol,
|
||||||
|
httpRedirectCode, replaceKeyPrefixWith,
|
||||||
|
replaceKeyWith, prefixFromRule } = routingInfo;
|
||||||
|
|
||||||
|
const redirectProtocol = protocol || encrypted ? 'https' : 'http';
|
||||||
|
const redirectCode = httpRedirectCode || 301;
|
||||||
|
const redirectHostName = hostName || hostHeader;
|
||||||
|
|
||||||
|
setCommonResponseHeaders(corsHeaders, response, log);
|
||||||
|
|
||||||
|
let redirectKey = objectKey;
|
||||||
|
// will only have either replaceKeyWith defined or replaceKeyPrefixWith
|
||||||
|
// defined. not both and might have neither
|
||||||
|
if (replaceKeyWith !== undefined) {
|
||||||
|
redirectKey = replaceKeyWith;
|
||||||
|
}
|
||||||
|
if (replaceKeyPrefixWith !== undefined) {
|
||||||
|
if (prefixFromRule !== undefined) {
|
||||||
|
// if here with prefixFromRule defined, means that
|
||||||
|
// passed condition
|
||||||
|
// and objectKey starts with this prefix. replace just first
|
||||||
|
// instance in objectKey with the replaceKeyPrefixWith value
|
||||||
|
redirectKey = objectKey.replace(prefixFromRule,
|
||||||
|
replaceKeyPrefixWith);
|
||||||
|
} else {
|
||||||
|
redirectKey = replaceKeyPrefixWith + objectKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let redirectLocation = justPath ? `/${redirectKey}` :
|
||||||
|
`${redirectProtocol}://${redirectHostName}/${redirectKey}`;
|
||||||
|
if (!redirectKey && redirectLocationHeader) {
|
||||||
|
// remove hanging slash
|
||||||
|
redirectLocation = redirectLocation.slice(0, -1);
|
||||||
|
}
|
||||||
|
log.end().info('redirecting request', {
|
||||||
|
httpCode: redirectCode,
|
||||||
|
redirectLocation: hostName,
|
||||||
|
});
|
||||||
|
response.writeHead(redirectCode, {
|
||||||
|
Location: redirectLocation,
|
||||||
|
});
|
||||||
|
response.end();
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = routesUtils;
|
Loading…
Reference in New Issue