UNPKG

juejin-leanengine

Version:
413 lines (352 loc) 12.7 kB
'use strict'; var connect = require('connect'); var bodyParser = require('body-parser'); var https = require('https'); var timeout = require('connect-timeout'); var _ = require('underscore'); var AV = require('./storage-extra'); var utils = require('./utils'); var NODE_ENV = process.env.NODE_ENV || 'development'; AV.express = function(options) { var router = connect(); router.use(require('../middleware/health-check')()); ['1', '1.1'].forEach(function(apiVersion) { router.use('/' + apiVersion + '/call', function(req, res, next) { req.rpcCall = true; next(); }); ['functions', 'call'].forEach(function(urlEndpoint) { router.use('/' + apiVersion + '/' + urlEndpoint, createCloudFunctionRouter(options)); }); }); return router; }; AV.koa = function(options) { return require('../middleware/koa')(AV); }; // override AV.Cloud to a connect app if (!AV._old_Cloud) { AV._old_Cloud = AV.Cloud; var defaultMiddleware; AV.Cloud = function() { if (!defaultMiddleware) { console.error('Use AV.Cloud as a middleware is deprecated, use AV.express() instead'); defaultMiddleware = AV.express(); } defaultMiddleware.apply(this, arguments); }; for (var key in AV._old_Cloud) { AV.Cloud[key] = AV._old_Cloud[key]; } } var Cloud = _.extend(AV.Cloud, require('./cloud')); // Don't reject unauthorized ssl. if (https.globalAgent && https.globalAgent.options) { https.globalAgent.options.rejectUnauthorized = false; } AV.Cloud.CookieSession = function(options) { if (options && options.framework == 'koa') { return require('../middleware/cookie-session-koa')(AV)(options); } else { return require('../middleware/cookie-session')(AV)(options); } }; AV.Cloud.HttpsRedirect = function(options) { if (options && options.framework == 'koa') { return require('../middleware/https-redirect-koa')(AV)(options); } else { return require('../middleware/https-redirect')(AV)(options); } } function createCloudFunctionRouter(options) { options = options || {}; var cloudFunctions = connect(); cloudFunctions.use(timeout(options.timeout || '15s')); cloudFunctions.use(bodyParser.urlencoded({extended: false, limit: '20mb'})); cloudFunctions.use(bodyParser.json({limit: '20mb'})); cloudFunctions.use(bodyParser.text({limit: '20mb'})); cloudFunctions.use(require('../middleware/cors')()); cloudFunctions.use(require('../middleware/domain-wrapper')()); cloudFunctions.use(require('../middleware/parse-leancloud-headers')(AV, {restrict: true})); cloudFunctions.use('/_ops/metadatas', function(req, res) { if (req.AV.authMasterKey) { return resp(res, Object.keys(Cloud.__code)); } else { return utils.unauthResp(res); } }); var fetchUserMiddleware = require('../middleware/fetch-user')(AV); cloudFunctions.use(function(req, res, next) { if (req.url === '/') { return respError(res, 'no function or class name'); } var fetchUser = function(callback) { fetchUserMiddleware(req, res, function(err) { if (err) { next(err); } else { callback(); } }); }; var sendResponse = function(err, data, isBare) { if (err) { return respError(res, err); } if (isBare) { return respBare(res, data); } else { return resp(res, data); } }; var splited = req.url.split('/'); if (splited.length == 2) { // cloud function if (_.contains([ '_messageReceived', '_receiversOffline', '_messageSent', '_conversationStart', '_conversationStarted', '_conversationAdd', '_conversationRemove', '_conversationUpdate' ], splited[1])) { if (!utils.verifyHookSign(AV.masterKey, splited[1], req.body.__sign)) { console.error('LeanEngine: verifyHookSign failed on', (req.originalUrl || req.url), 'from', utils.getRemoteAddress(req)); return utils.unauthResp(res); } } var handleCloudfunction = function() { call(splited[1], req.body, req.AV.user, req, { decodeAVObject: req.rpcCall }, function(err, data) { sendResponse(err, data); }); }; if (Cloud.__code[splited[1]] && Cloud.__code[splited[1]].fetchUser === false) { handleCloudfunction(); } else { fetchUser(handleCloudfunction); } } else if (splited.length == 3) { // class hook var userObj = new AV.User(); if (splited[1] === 'onVerified') { if (utils.verifyHookSign(AV.masterKey, '__on_verified_' + splited[2], req.body.object.__sign)) { userObj._finishFetch(req.body.object, true); onVerified(req, splited[2], userObj); sendResponse(null, 'ok'); } else { console.error('LeanEngine: verifyHookSign failed on', (req.originalUrl || req.url), 'from', utils.getRemoteAddress(req)); return utils.unauthResp(res); } } else if (splited[1] === '_User' && splited[2] === 'onLogin') { if (utils.verifyHookSign(AV.masterKey, '__on_login__User', req.body.object.__sign)) { userObj._finishFetch(req.body.object, true); onLogin(req, userObj, sendResponse); } else { console.error('LeanEngine: verifyHookSign failed on', (req.originalUrl || req.url), 'from', utils.getRemoteAddress(req)); return utils.unauthResp(res); } } else if ((splited[1] === 'BigQuery' || splited[1] === 'Insight' ) && splited[2] === 'onComplete') { if (utils.verifyHookSign(AV.masterKey, '__on_complete_bigquery_job', req.body.__sign)) { onCompleteBigQueryJob(req.body); sendResponse(null, 'ok'); } else { console.error('LeanEngine: verifyHookSign failed on', (req.originalUrl || req.url), 'from', utils.getRemoteAddress(req)); return utils.unauthResp(res); } } else { var verified = false; if (splited[2].indexOf('after') === 0) { verified = utils.verifyHookSign(AV.masterKey, '__after_for_' + splited[1], req.body.object.__after); } else { verified = utils.verifyHookSign(AV.masterKey, '__before_for_' + splited[1], req.body.object.__before); } if (!verified) { console.error('LeanEngine: verifyHookSign failed on', (req.originalUrl || req.url), 'from', utils.getRemoteAddress(req)); return utils.unauthResp(res); } if (req.body.user) { userObj._finishFetch(req.body.user, true); req.AV.user = userObj; } classHook(splited[1], hookNameMapping[splited[2]], req.body.object, userObj, req, sendResponse); } } }); cloudFunctions.use(function(err, req, res, next) { // jshint ignore:line if(req.timedout) { console.error('LeanEngine function timeout, url=%s, timeout=%d', req.originalUrl, err.timeout); err.code = 124; // https://leancloud.cn/docs/error_code.html#_124 err.message = 'The request timed out on the server.'; } respError(res, err); }); return cloudFunctions; } var resp = function(res, data) { res.setHeader('Content-Type', 'application/json; charset=UTF-8'); res.statusCode = 200; return res.end(JSON.stringify({result: data})); }; var respBare = function(res, data) { res.setHeader('Content-Type', 'application/json; charset=UTF-8'); res.statusCode = 200; return res.end(JSON.stringify(data)); }; var respError = function(res, err) { res.setHeader('Content-Type', 'application/json; charset=UTF-8'); res.statusCode = err.statusCode || 400; res.end(JSON.stringify({ code: err.code || 1, error: err && (err.message || err.responseText || err) || 'null message' })); }; var call = function(funcName, params, user, req, options, cb) { if (!options) { options = {}; } if (!Cloud.__code[funcName]) { var err = new Error("LeanEngine not found function named '" + funcName + "' for app '" + AV.applicationId + "' on " + NODE_ENV + "."); err.statusCode = 404; return cb(err); } try { if (options.decodeAVObject) { params = decodeParams(params); } var request = utils.prepareRequestObject({ user: user, params: params, req: req }); var response = utils.prepareResponseObject(req.res, function(err, result) { if (!err && options.decodeAVObject) { result = encodeResult(result); } cb(err, result); }); Cloud.__code[funcName](request, response); } catch (err) { console.warn('Execute \'' + funcName + '\' failed with error: ' + (err.stack || err)); err.statusCode = 500; return cb(err); } }; var classHook = function(className, hook, object, user, req, cb) { if (!Cloud.__code[hook + className]) { var err = new Error("LeanEngine could not find hook '" + hook + className + "' for app '" + AV.applicationId + "' on " + NODE_ENV + "."); err.statusCode = 404; return cb(err); } var obj = decodeParams(_.extend({}, object, { __type: 'Object', className: className })); // for beforeUpdate if (object._updatedKeys) { obj.updatedKeys = object._updatedKeys; } try { var request = utils.prepareRequestObject({ user: user, object: obj, req: req }); if (hook.indexOf('__after_') === 0) { setHookMark('__after', obj); // after 的 hook 不需要 response 参数,并且请求默认返回 ok Cloud.__code[hook + className](request); return cb(null, 'ok'); } else { setHookMark('__before', obj); Cloud.__code[hook + className](request, utils.prepareResponseObject(req.res, function(err) { if (err) { cb(new Error(err)); } else if ('__before_delete_for_' === hook) { cb(null, {}, true); } else { cb(null, obj, true); } })); } } catch (err) { console.warn('Execute \'' + hook + className + '\' failed with error: ' + (err.stack || err)); if (hook.indexOf('__after__') === 0) { return cb(null, 'ok'); } err.statusCode = 500; return cb(err); } }; var setHookMark = function(hookAction, obj) { var sign = obj.get(hookAction); if(sign) { obj.set(hookAction, sign); } else { if(hookAction.indexOf('__before') === 0) { obj.disableBeforeHook(); } else if(hookAction.indexOf('__after') === 0) { obj.disableAfterHook(); } } }; var onVerified = function(req, type, user) { try { var request = utils.prepareRequestObject({ user: user, object: user, req: req }); Cloud.__code['__on_verified_' + type](request); } catch (err) { console.warn('Execute onVerified ' + type + ' failed with error: ' + (err.stack || err)); } }; var onLogin = function(req, user, cb) { try { var request = utils.prepareRequestObject({ user: user, object: user, req: req }); var response = utils.prepareResponseObject(req.res, function(err) { if (err) { cb(new Error(err)); } else { cb(null, 'ok'); } }); Cloud.__code.__on_login__User(request, response); } catch (err) { console.warn('Execute onLogin failed with error: ' + (err.stack || err)); } }; var onCompleteBigQueryJob = function(data) { try { Cloud.__code.__on_complete_bigquery_job(null, data); } catch (err) { console.warn('Execute onCompleteBigQueryJob failed with error: ' + (err.stack || err)); } }; var hookNameMapping = { beforeSave: '__before_save_for_', beforeUpdate: '__before_update_for_', afterSave: '__after_save_for_', afterUpdate: '__after_update_for_', beforeDelete: '__before_delete_for_', afterDelete: '__after_delete_for_' }; var encodeResult = function(result) { var encodeAVObject = function(object) { if (object && object._toFullJSON){ object = object._toFullJSON([]); } return _.mapObject(object, function(value) { return AV._encode(value, []); }); }; if (_.isArray(result)) { return result.map(function(object) { return encodeAVObject(object); }); } else { return encodeAVObject(result); } }; var decodeParams = AV._decode; module.exports = AV;