first commit

This commit is contained in:
2023-05-19 00:42:48 +08:00
commit 53de9c6c51
243 changed files with 39485 additions and 0 deletions

View File

@@ -0,0 +1,125 @@
'use strict';
const AuthApi = require('../auth/auth_api');
const basicAuth = require('basic-auth');
module.exports = function authorization (metadataBackend, forceToBeMaster = false) {
return function authorizationMiddleware (req, res, next) {
const { user } = res.locals;
const credentials = getCredentialsFromRequest(req);
if (!userMatches(credentials, user)) {
if (req.profiler) {
req.profiler.done('authorization');
}
return next(new Error('permission denied'));
}
res.locals.api_key = credentials.apiKeyToken;
const params = Object.assign({ metadataBackend }, res.locals, req.query, req.body);
const authApi = new AuthApi(req, params);
authApi.verifyCredentials(function (err, authorizationLevel) {
if (req.profiler) {
req.profiler.done('authorization');
}
if (err) {
return next(err);
}
res.locals.authorizationLevel = authorizationLevel;
if (forceToBeMaster && authorizationLevel !== 'master') {
return next(new Error('permission denied'));
}
res.set('vary', 'Authorization'); //Honor Authorization header when caching.
next();
});
};
};
const credentialsGetters = [
getCredentialsFromHeaderAuthorization,
getCredentialsFromRequestQueryString,
getCredentialsFromRequestBody,
];
function getCredentialsFromRequest (req) {
let credentials = null;
for (var getter of credentialsGetters) {
credentials = getter(req);
if (apiKeyTokenFound(credentials)) {
break;
}
}
return credentials;
}
function getCredentialsFromHeaderAuthorization(req) {
const { pass, name } = basicAuth(req) || {};
if (pass !== undefined && name !== undefined) {
return {
apiKeyToken: pass,
user: name
};
}
return false;
}
function getCredentialsFromRequestQueryString(req) {
if (req.query.api_key) {
return {
apiKeyToken: req.query.api_key
};
}
if (req.query.map_key) {
return {
apiKeyToken: req.query.map_key
};
}
return false;
}
function getCredentialsFromRequestBody(req) {
if (req.body && req.body.api_key) {
return {
apiKeyToken: req.body.api_key
};
}
if (req.body && req.body.map_key) {
return {
apiKeyToken: req.body.map_key
};
}
return false;
}
function apiKeyTokenFound(credentials) {
if (typeof credentials === 'boolean') {
return credentials;
}
if (credentials.apiKeyToken !== undefined) {
return true;
}
return false;
}
function userMatches (credentials, user) {
return !(credentials.user !== undefined && credentials.user !== user);
}

View File

@@ -0,0 +1,146 @@
'use strict';
/*!
* Connect - bodyParser
* Copyright(c) 2010 Sencha Inc.
* Copyright(c) 2011 TJ Holowaychuk
* MIT Licensed
*/
/**
* Module dependencies.
*/
var qs = require('qs');
var multer = require('multer');
/**
* Extract the mime type from the given request's
* _Content-Type_ header.
*
* @param {IncomingMessage} req
* @return {String}
* @api private
*/
function mime(req) {
var str = req.headers['content-type'] || '';
return str.split(';')[0];
}
/**
* Parse request bodies.
*
* By default _application/json_, _application/x-www-form-urlencoded_,
* and _multipart/form-data_ are supported, however you may map `connect.bodyParser.parse[contentType]`
* to a function receiving `(req, options, callback)`.
*
* Examples:
*
* connect.createServer(
* connect.bodyParser()
* , function(req, res) {
* res.end('viewing user ' + req.body.user.name);
* }
* );
*
* $ curl -d 'user[name]=tj' http://localhost/
* $ curl -d '{"user":{"name":"tj"}}' -H "Content-Type: application/json" http://localhost/
*
* Multipart req.files:
*
* As a security measure files are stored in a separate object, stored
* as `req.files`. This prevents attacks that may potentially alter
* filenames, and depending on the application gain access to restricted files.
*
* Multipart configuration:
*
* The `options` passed are provided to each parser function.
* The _multipart/form-data_ parser merges these with formidable's
* IncomingForm object, allowing you to tweak the upload directory,
* size limits, etc. For example you may wish to retain the file extension
* and change the upload directory:
*
* server.use(bodyParser({ uploadDir: '/www/mysite.com/uploads' }));
*
* View [node-formidable](https://github.com/felixge/node-formidable) for more information.
*
* If you wish to use formidable directly within your app, and do not
* desire this behaviour for multipart requests simply remove the
* parser:
*
* delete connect.bodyParser.parse['multipart/form-data'];
*
* Or
*
* delete express.bodyParser.parse['multipart/form-data'];
*
* @param {Object} options
* @return {Function}
* @api public
*/
exports = module.exports = function bodyParser(options){
options = options || {};
return function bodyParser(req, res, next) {
if (req.body) {
return next();
}
req.body = {};
if ('GET' === req.method || 'HEAD' === req.method) {
return next();
}
var parser = exports.parse[mime(req)];
if (parser) {
parser(req, options, next);
} else {
next();
}
};
};
/**
* Parsers.
*/
exports.parse = {};
/**
* Parse application/x-www-form-urlencoded.
*/
exports.parse['application/x-www-form-urlencoded'] = function(req, options, fn){
var buf = '';
req.setEncoding('utf8');
req.on('data', function(chunk){ buf += chunk; });
req.on('end', function(){
try {
req.body = buf.length ? qs.parse(buf) : {};
fn();
} catch (err){
fn(err);
}
});
};
/**
* Parse application/json.
*/
exports.parse['application/json'] = function(req, options, fn){
var buf = '';
req.setEncoding('utf8');
req.on('data', function(chunk){ buf += chunk; });
req.on('end', function(){
try {
req.body = buf.length ? JSON.parse(buf) : {};
fn();
} catch (err){
fn(err);
}
});
};
var multipartMiddleware = multer({ limits: { fieldSize: Infinity } });
exports.parse['multipart/form-data'] = multipartMiddleware.none();

View File

@@ -0,0 +1,23 @@
'use strict';
module.exports = function connectionParams (userDatabaseService) {
return function connectionParamsMiddleware (req, res, next) {
const { user, api_key: apikeyToken, authorizationLevel } = res.locals;
userDatabaseService.getConnectionParams(user, apikeyToken, authorizationLevel,
function (err, userDbParams, authDbParams) {
if (req.profiler) {
req.profiler.done('getConnectionParams');
}
if (err) {
return next(err);
}
res.locals.userDbParams = userDbParams;
res.locals.authDbParams = authDbParams;
next();
});
};
};

16
app/middlewares/cors.js Normal file
View File

@@ -0,0 +1,16 @@
'use strict';
module.exports = function cors(extraHeaders) {
return function(req, res, next) {
var baseHeaders = 'X-Requested-With, X-Prototype-Version, X-CSRF-Token, Authorization';
if(extraHeaders) {
baseHeaders += ', ' + extraHeaders;
}
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', baseHeaders);
next();
};
};

View File

@@ -0,0 +1,26 @@
'use strict';
const PSQL = require('cartodb-psql');
const remainingQuotaQuery = 'SELECT _CDB_UserQuotaInBytes() - CDB_UserDataSize(current_schema()) AS remaining_quota';
module.exports = function dbQuota () {
return function dbQuotaMiddleware (req, res, next) {
const { userDbParams } = res.locals;
const pg = new PSQL(userDbParams);
pg.connect((err, client, done) => {
if (err) {
return next(err);
}
client.query(remainingQuotaQuery, (err, result) => {
if(err) {
return next(err);
}
const remainingQuota = result.rows[0].remaining_quota;
res.locals.dbRemainingQuota = remainingQuota;
done();
next();
});
});
};
};

91
app/middlewares/error.js Normal file
View File

@@ -0,0 +1,91 @@
'use strict';
const errorHandlerFactory = require('../services/error_handler_factory');
const MAX_ERROR_STRING_LENGTH = 1024;
module.exports = function error() {
return function errorMiddleware(err, req, res, next) {
const errorHandler = errorHandlerFactory(err);
let errorResponse = errorHandler.getResponse();
if (global.settings.environment === 'development') {
errorResponse.stack = err.stack;
}
if (global.settings.environment !== 'test') {
// TODO: email this Exception report
console.error("EXCEPTION REPORT: " + err.stack);
}
// Force inline content disposition
res.header("Content-Disposition", 'inline');
if (req && req.profiler) {
req.profiler.done('finish');
res.header('X-SQLAPI-Profiler', req.profiler.toJSONString());
}
setErrorHeader(errorHandler, res);
res.header('Content-Type', 'application/json; charset=utf-8');
res.status(getStatusError(errorHandler, req));
if (req.query && req.query.callback) {
res.jsonp(errorResponse);
} else {
res.json(errorResponse);
}
if (req && req.profiler) {
res.req.profiler.sendStats();
}
next();
};
};
function getStatusError(errorHandler, req) {
let statusError = errorHandler.http_status;
// JSONP has to return 200 status error
if (req && req.query && req.query.callback) {
statusError = 200;
}
return statusError;
}
function setErrorHeader(errorHandler, res) {
const errorsLog = {
context: errorHandler.context,
detail: errorHandler.detail,
hint: errorHandler.hint,
statusCode: errorHandler.http_status,
message: errorHandler.message
};
res.set('X-SQLAPI-Errors', stringifyForLogs(errorsLog));
}
/**
* Remove problematic nested characters
* from object for logs RegEx
*
* @param {Object} object
*/
function stringifyForLogs(object) {
Object.keys(object).map(key => {
if (typeof object[key] === 'string') {
object[key] = object[key]
.substring(0, MAX_ERROR_STRING_LENGTH)
.replace(/[^a-zA-Z0-9]/g, ' ');
} else if (typeof object[key] === 'object') {
stringifyForLogs(object[key]);
} else if (object[key] instanceof Array) {
for (let element of object[key]) {
stringifyForLogs(element);
}
}
});
return JSON.stringify(object);
}

View File

@@ -0,0 +1,24 @@
'use strict';
module.exports.initializeProfilerMiddleware = function initializeProfiler (label) {
return function initializeProfilerMiddleware (req, res, next) {
if (req.profiler) {
req.profiler.start(`sqlapi.${label}`);
}
next();
};
};
module.exports.finishProfilerMiddleware = function finishProfiler () {
return function finishProfilerMiddleware (req, res, next) {
if (req.profiler) {
req.profiler.end();
req.profiler.sendStats();
res.header('X-SQLAPI-Profiler', req.profiler.toJSONString());
}
next();
};
};

View File

@@ -0,0 +1,61 @@
'use strict';
const RATE_LIMIT_ENDPOINTS_GROUPS = {
QUERY: 'query',
JOB_CREATE: 'job_create',
JOB_GET: 'job_get',
JOB_DELETE: 'job_delete',
COPY_FROM: 'copy_from',
COPY_TO: 'copy_to'
};
function rateLimit(userLimits, endpointGroup = null) {
if (!isRateLimitEnabled(endpointGroup)) {
return function rateLimitDisabledMiddleware(req, res, next) { next(); };
}
return function rateLimitMiddleware(req, res, next) {
userLimits.getRateLimit(res.locals.user, endpointGroup, function(err, userRateLimit) {
if (err) {
return next(err);
}
if (!userRateLimit) {
return next();
}
const [isBlocked, limit, remaining, retry, reset] = userRateLimit;
res.set({
'Carto-Rate-Limit-Limit': limit,
'Carto-Rate-Limit-Remaining': remaining,
'Carto-Rate-Limit-Reset': reset
});
if (isBlocked) {
// retry is floor rounded in seconds by redis-cell
res.set('Retry-After', retry + 1);
const rateLimitError = new Error(
'You are over platform\'s limits. Please contact us to know more details'
);
rateLimitError.http_status = 429;
rateLimitError.context = 'limit';
rateLimitError.detail = 'rate-limit';
return next(rateLimitError);
}
return next();
});
};
}
function isRateLimitEnabled(endpointGroup) {
return global.settings.ratelimits.rateLimitsEnabled &&
endpointGroup &&
global.settings.ratelimits.endpoints[endpointGroup];
}
module.exports = rateLimit;
module.exports.RATE_LIMIT_ENDPOINTS_GROUPS = RATE_LIMIT_ENDPOINTS_GROUPS;

View File

@@ -0,0 +1,25 @@
'use strict';
module.exports = function timeoutLimits (metadataBackend) {
return function timeoutLimitsMiddleware (req, res, next) {
const { user, authorizationLevel } = res.locals;
metadataBackend.getUserTimeoutRenderLimits(user, function (err, timeoutRenderLimit) {
if (req.profiler) {
req.profiler.done('getUserTimeoutLimits');
}
if (err) {
return next(err);
}
const userLimits = {
timeout: (authorizationLevel === 'master') ? timeoutRenderLimit.render : timeoutRenderLimit.renderPublic
};
res.locals.userLimits = userLimits;
next();
});
};
};

38
app/middlewares/user.js Normal file
View File

@@ -0,0 +1,38 @@
'use strict';
const CdbRequest = require('../models/cartodb_request');
module.exports = function user(metadataBackend) {
const cdbRequest = new CdbRequest();
return function userMiddleware (req, res, next) {
res.locals.user = getUserNameFromRequest(req, cdbRequest);
checkUserExists(metadataBackend, res.locals.user, function(err, userExists) {
if (err || !userExists) {
const error = new Error('Unauthorized');
error.type = 'auth';
error.subtype = 'user-not-found';
error.http_status = 404;
error.message = errorUserNotFoundMessageTemplate(res.locals.user);
next(error);
}
return next();
});
};
};
function getUserNameFromRequest(req, cdbRequest) {
return cdbRequest.userByReq(req);
}
function checkUserExists(metadataBackend, userName, callback) {
metadataBackend.getUserId(userName, function(err) {
callback(err, !err);
});
}
function errorUserNotFoundMessageTemplate(user) {
return `Sorry, we can't find CARTO user '${user}'. Please check that you have entered the correct domain.`;
}