Add wrapper to make Meteor methods calls over REST (#17092)

pull/17035/head
Diego Sampaio 5 years ago committed by GitHub
parent 3cb432da47
commit 1ab3ae4869
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      app/api/server/api.js
  2. 6
      app/api/server/settings.js
  3. 55
      app/api/server/v1/misc.js
  4. 2
      app/livechat/client/views/app/tabbar/visitorInfo.js
  5. 8
      app/ui-cached-collection/client/models/CachedCollection.js
  6. 7
      app/ui-master/server/inject.js
  7. 4
      app/ui-utils/client/lib/RoomManager.js
  8. 2
      app/utils/stream/constants.js
  9. 65
      client/lib/meteorCallWrapper.ts
  10. 1
      client/main.js
  11. 28
      client/meteor.d.ts
  12. 8
      client/window.d.ts
  13. 2
      packages/rocketchat-i18n/i18n/en.i18n.json
  14. 9
      server/main.d.ts
  15. 14
      server/stream/rooms/index.js

@ -326,9 +326,9 @@ export class APIClass extends Restivus {
});
logger.debug(`${ this.request.method.toUpperCase() }: ${ this.request.url }`);
const requestIp = getRequestIP(this.request);
this.requestIp = getRequestIP(this.request);
const objectForRateLimitMatch = {
IPAddr: requestIp,
IPAddr: this.requestIp,
route: `${ this.request.route }${ this.request.method.toLowerCase() }`,
};
let result;
@ -338,7 +338,7 @@ export class APIClass extends Restivus {
close() {},
token: this.token,
httpHeaders: this.request.headers,
clientAddress: requestIp,
clientAddress: this.requestIp,
};
try {

@ -11,5 +11,11 @@ settings.addGroup('General', function() {
this.add('API_Shield_user_require_auth', false, { type: 'boolean', public: false, enableQuery: { _id: 'API_Enable_Shields', value: true } });
this.add('API_Enable_CORS', false, { type: 'boolean', public: false });
this.add('API_CORS_Origin', '*', { type: 'string', public: false, enableQuery: { _id: 'API_Enable_CORS', value: true } });
this.add('API_Use_REST_For_DDP_Calls', false, {
type: 'boolean',
public: true,
alert: 'API_Use_REST_For_DDP_Calls_Alert',
});
});
});

@ -1,6 +1,10 @@
import crypto from 'crypto';
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { EJSON } from 'meteor/ejson';
import { DDPRateLimiter } from 'meteor/ddp-rate-limiter';
import s from 'underscore.string';
import { hasRole, hasPermission } from '../../../authorization/server';
@ -216,3 +220,54 @@ API.v1.addRoute('stdout.queue', { authRequired: true }, {
return API.v1.success({ queue: StdOut.queue });
},
});
const mountResult = ({ id, error, result }) => ({
message: EJSON.stringify({
msg: 'result',
id,
error,
result,
}),
});
const methodCall = () => ({
post() {
check(this.bodyParams, {
message: String,
});
const { method, params, id } = EJSON.parse(this.bodyParams.message);
const connectionId = this.token || crypto.createHash('md5').update(this.requestIp + this.request.headers['user-agent']).digest('hex');
const rateLimiterInput = {
userId: this.userId,
clientAddress: this.requestIp,
type: 'method',
name: method,
connectionId,
};
try {
DDPRateLimiter._increment(rateLimiterInput);
const rateLimitResult = DDPRateLimiter._check(rateLimiterInput);
if (!rateLimitResult.allowed) {
throw new Meteor.Error(
'too-many-requests',
DDPRateLimiter.getErrorMessage(rateLimitResult),
{ timeToReset: rateLimitResult.timeToReset },
);
}
const result = Meteor.call(method, ...params);
return API.v1.success(mountResult({ id, result }));
} catch (error) {
return API.v1.success(mountResult({ id, error }));
}
},
});
// had to create two different endpoints for authenticated and non-authenticated calls
// because restivus does not provide 'this.userId' if 'authRequired: false'
API.v1.addRoute('method.call/:method', { authRequired: true, rateLimiterOptions: false }, methodCall());
API.v1.addRoute('method.callAnon/:method', { authRequired: false, rateLimiterOptions: false }, methodCall());

@ -344,8 +344,8 @@ Template.visitorInfo.onCreated(function() {
};
if (rid) {
loadRoomData(rid);
RoomManager.roomStream.on(rid, this.updateRoom);
loadRoomData(rid);
}
this.autorun(async () => {

@ -11,7 +11,7 @@ import EventEmitter from 'wolfy87-eventemitter';
import { callbacks } from '../../../callbacks';
import Notifications from '../../../notifications/client/lib/Notifications';
import { getConfig } from '../../../ui-utils/client/config';
import { callMethod } from '../../../ui-utils/client/lib/callMethod';
const fromEntries = Object.fromEntries || function fromEntries(iterable) {
return [...iterable].reduce((obj, { 0: key, 1: val }) => Object.assign(obj, { [key]: val }), {});
@ -26,8 +26,6 @@ const wrap = (fn) => (...args) => new Promise((resolve, reject) => {
});
});
const call = wrap(Meteor.call);
const localforageGetItem = wrap(localforage.getItem);
class CachedCollectionManagerClass extends EventEmitter {
@ -219,7 +217,7 @@ export class CachedCollection extends EventEmitter {
async loadFromServer() {
const startTime = new Date();
const lastTime = this.updatedAt;
const data = await call(this.methodName);
const data = await callMethod(this.methodName);
this.log(`${ data.length } records loaded from server`);
data.forEach((record) => {
callbacks.run(`cachedCollection-loadFromServer-${ this.name }`, record, 'changed');
@ -316,7 +314,7 @@ export class CachedCollection extends EventEmitter {
this.log(`syncing from ${ this.updatedAt }`);
const data = await call(this.syncMethodName, this.updatedAt);
const data = await callMethod(this.syncMethodName, this.updatedAt);
let changes = [];
if (data.update && data.update.length > 0) {

@ -51,6 +51,13 @@ Meteor.startup(() => {
`);
}
settings.get('API_Use_REST_For_DDP_Calls', (key, value) => {
if (!value) {
return injectIntoHead(key, '');
}
injectIntoHead(key, '<script>window.USE_REST_FOR_DDP_CALLS = true;</script>');
});
settings.get('Assets_SvgFavicon_Enable', (key, value) => {
const standardFavicons = `
<link rel="icon" sizes="16x16" type="image/png" href="assets/favicon_16.png" />

@ -15,7 +15,7 @@ import { Notifications } from '../../../notifications';
import { CachedChatRoom, ChatMessage, ChatSubscription, CachedChatSubscription } from '../../../models';
import { CachedCollectionManager } from '../../../ui-cached-collection';
import { getConfig } from '../config';
import { ROOM_DATA_STREAM_OBSERVER } from '../../../utils/stream/constants';
import { ROOM_DATA_STREAM } from '../../../utils/stream/constants';
import { call } from '..';
@ -45,7 +45,7 @@ const onDeleteMessageBulkStream = ({ rid, ts, excludePinned, ignoreDiscussion, u
export const RoomManager = new function() {
const openedRooms = {};
const msgStream = new Meteor.Streamer('room-messages');
const roomStream = new Meteor.Streamer(ROOM_DATA_STREAM_OBSERVER);
const roomStream = new Meteor.Streamer(ROOM_DATA_STREAM);
const onlineUsers = new ReactiveVar({});
const Dep = new Tracker.Dependency();
const Cls = class {

@ -1 +1 @@
export const ROOM_DATA_STREAM_OBSERVER = 'room-data-observer';
export const ROOM_DATA_STREAM = 'room-data';

@ -0,0 +1,65 @@
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { DDPCommon } from 'meteor/ddp-common';
import { APIClient } from '../../app/utils/client';
const bypassMethods: string[] = [
'setUserStatus',
];
function shouldBypass({ method, params }: Meteor.IDDPMessage): boolean {
if (method === 'login' && params[0]?.resume) {
return true;
}
if (method.startsWith('UserPresence:') || bypassMethods.includes(method)) {
return true;
}
if (method.startsWith('stream-')) {
return true;
}
return false;
}
function wrapMeteorDDPCalls(): void {
const { _send } = Meteor.connection;
Meteor.connection._send = function _DDPSendOverREST(message): void {
if (message.msg !== 'method' || shouldBypass(message)) {
return _send.call(Meteor.connection, message);
}
const endpoint = Tracker.nonreactive(() => (!Meteor.userId() ? 'method.callAnon' : 'method.call'));
const restParams = {
message: DDPCommon.stringifyDDP(message),
};
const processResult = (_message: any): void => {
Meteor.connection._livedata_data({
msg: 'updated',
methods: [message.id],
});
Meteor.connection.onMessage(_message);
};
APIClient.v1.post(`${ endpoint }/${ encodeURIComponent(message.method) }`, restParams)
.then(({ message: _message }) => {
processResult(_message);
if (message.method === 'login') {
const parsedMessage = DDPCommon.parseDDP(_message);
if (parsedMessage.result?.token) {
Meteor.loginWithToken(parsedMessage.result.token);
}
}
})
.catch((error) => {
console.error(error);
});
};
}
window.USE_REST_FOR_DDP_CALLS && wrapMeteorDDPCalls();

@ -1,6 +1,7 @@
import '@rocket.chat/fuselage-polyfills';
import 'url-polyfill';
import './lib/meteorCallWrapper';
import './importsCss';
import './importPackages';
import '../imports/startup/client';

28
client/meteor.d.ts vendored

@ -0,0 +1,28 @@
/* eslint-disable @typescript-eslint/camelcase */
import { EJSON } from 'meteor/ejson';
declare module 'meteor/meteor' {
namespace Meteor {
interface IDDPMessage {
msg: 'method';
method: string;
params: EJSON[];
id: string;
}
interface IDDPUpdatedMessage {
msg: 'updated';
methods: string[];
}
interface IMeteorConnection {
_send(message: IDDPMessage): void;
_livedata_data(message: IDDPUpdatedMessage): void;
onMessage(message: string): void;
}
const connection: IMeteorConnection;
}
}

@ -0,0 +1,8 @@
export {};
declare global {
// eslint-disable-next-line @typescript-eslint/interface-name-prefix
interface Window {
USE_REST_FOR_DDP_CALLS?: boolean;
}
}

@ -359,6 +359,8 @@
"API_Tokenpass_URL_Description": "Example: https://domain.com (excluding trailing slash)",
"API_Upper_Count_Limit": "Max Record Amount",
"API_Upper_Count_Limit_Description": "What is the maximum number of records the REST API should return (when not unlimited)?",
"API_Use_REST_For_DDP_Calls": "Use REST instead of websocket for Meteor calls",
"API_Use_REST_For_DDP_Calls_Alert": "This is an experimental and temporary feature. It forces web client and mobile app to use REST requests instead of using websockets for Meteor method calls.",
"API_User_Limit": "User Limit for Adding All Users to Channel",
"API_Wordpress_URL": "WordPress URL",
"Apiai_Key": "Api.ai Key",

9
server/main.d.ts vendored

@ -1,3 +1,5 @@
import { EJSON } from 'meteor/ejson';
/* eslint-disable @typescript-eslint/interface-name-prefix */
declare module 'meteor/random' {
namespace Random {
@ -33,6 +35,13 @@ declare module 'meteor/meteor' {
}
}
declare module 'meteor/ddp-common' {
namespace DDPCommon {
function stringifyDDP(msg: EJSON): string;
function parseDDP(msg: string): EJSON;
}
}
declare module 'meteor/rocketchat:tap-i18n' {
namespace TAPi18n {
function __(s: string, options: { lng: string }): string;

@ -1,11 +1,9 @@
import { Meteor } from 'meteor/meteor';
import { roomTypes } from '../../../app/utils';
import { ROOM_DATA_STREAM_OBSERVER } from '../../../app/utils/stream/constants';
import { ROOM_DATA_STREAM } from '../../../app/utils/stream/constants';
export const roomDataStream = new Meteor.Streamer(ROOM_DATA_STREAM_OBSERVER);
const isEmitAllowed = (t) => roomTypes.getConfig(t).isEmitAllowed();
export const roomDataStream = new Meteor.Streamer(ROOM_DATA_STREAM);
roomDataStream.allowWrite('none');
@ -16,11 +14,7 @@ roomDataStream.allowRead(function(rid) {
return false;
}
if (isEmitAllowed(room.t) === false) {
return false;
}
return true;
return roomTypes.getConfig(room.t).isEmitAllowed();
} catch (error) {
return false;
}
@ -31,7 +25,7 @@ export function emitRoomDataEvent(id, data) {
return;
}
if (isEmitAllowed(data.t) === false) {
if (!roomTypes.getConfig(data.t).isEmitAllowed()) {
return;
}

Loading…
Cancel
Save