@sockethub/server 5.0.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/LICENSE +165 -0
  2. package/README.md +136 -0
  3. package/bin/sockethub +3 -0
  4. package/coverage/tmp/coverage-93126-1649152190997-0.json +1 -0
  5. package/dist/bootstrap/init.d.ts +18 -0
  6. package/dist/bootstrap/init.js +63 -0
  7. package/dist/bootstrap/init.js.map +1 -0
  8. package/dist/bootstrap/platforms.js +75 -0
  9. package/dist/common.d.ts +3 -0
  10. package/dist/common.js +20 -0
  11. package/dist/common.js.map +1 -0
  12. package/dist/config.d.ts +6 -0
  13. package/dist/config.js +102 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/crypto.d.ts +10 -0
  16. package/dist/crypto.js +38 -0
  17. package/dist/crypto.js.map +1 -0
  18. package/dist/defaults.json +28 -0
  19. package/dist/index.d.ts +2 -0
  20. package/dist/index.js +25 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/janitor.d.ts +15 -0
  23. package/dist/janitor.js +89 -0
  24. package/dist/janitor.js.map +1 -0
  25. package/dist/listener.d.ts +28 -0
  26. package/dist/listener.js +91 -0
  27. package/dist/listener.js.map +1 -0
  28. package/dist/middleware/create-activity-object.d.ts +6 -0
  29. package/dist/middleware/create-activity-object.js +19 -0
  30. package/dist/middleware/create-activity-object.js.map +1 -0
  31. package/dist/middleware/expand-activity-stream.d.ts +2 -0
  32. package/dist/middleware/expand-activity-stream.js +33 -0
  33. package/dist/middleware/expand-activity-stream.js.map +1 -0
  34. package/dist/middleware/expand-activity-stream.test.data.d.ts +480 -0
  35. package/dist/middleware/expand-activity-stream.test.data.js +360 -0
  36. package/dist/middleware/expand-activity-stream.test.data.js.map +1 -0
  37. package/dist/middleware/store-credentials.d.ts +3 -0
  38. package/dist/middleware/store-credentials.js +19 -0
  39. package/dist/middleware/store-credentials.js.map +1 -0
  40. package/dist/middleware/validate.d.ts +2 -0
  41. package/dist/middleware/validate.js +58 -0
  42. package/dist/middleware/validate.js.map +1 -0
  43. package/dist/middleware/validate.test.data.d.ts +532 -0
  44. package/dist/middleware/validate.test.data.js +263 -0
  45. package/dist/middleware/validate.test.data.js.map +1 -0
  46. package/dist/middleware.d.ts +10 -0
  47. package/dist/middleware.js +54 -0
  48. package/dist/middleware.js.map +1 -0
  49. package/dist/platform-instance.d.ts +77 -0
  50. package/dist/platform-instance.js +211 -0
  51. package/dist/platform-instance.js.map +1 -0
  52. package/dist/platform.d.ts +6 -0
  53. package/dist/platform.js +187 -0
  54. package/dist/platform.js.map +1 -0
  55. package/dist/process-manager.d.ts +11 -0
  56. package/dist/process-manager.js +78 -0
  57. package/dist/process-manager.js.map +1 -0
  58. package/dist/routes.d.ts +13 -0
  59. package/dist/routes.js +83 -0
  60. package/dist/routes.js.map +1 -0
  61. package/dist/sockethub.d.ts +39 -0
  62. package/dist/sockethub.js +119 -0
  63. package/dist/sockethub.js.map +1 -0
  64. package/dist/store.d.ts +5 -0
  65. package/dist/store.js +17 -0
  66. package/dist/store.js.map +1 -0
  67. package/package.json +103 -0
  68. package/sockethub.config.example.json +33 -0
  69. package/src/bootstrap/init.d.ts +8 -0
  70. package/src/bootstrap/init.ts +83 -0
  71. package/src/bootstrap/platforms.js +75 -0
  72. package/src/common.test.ts +54 -0
  73. package/src/common.ts +14 -0
  74. package/src/config.d.ts +2 -0
  75. package/src/config.test.ts +28 -0
  76. package/src/config.ts +94 -0
  77. package/src/crypto.d.ts +5 -0
  78. package/src/crypto.test.ts +41 -0
  79. package/src/crypto.ts +41 -0
  80. package/src/defaults.json +28 -0
  81. package/src/index.ts +28 -0
  82. package/src/janitor.d.ts +8 -0
  83. package/src/janitor.ts +89 -0
  84. package/src/listener.ts +79 -0
  85. package/src/middleware/create-activity-object.test.ts +10 -0
  86. package/src/middleware/create-activity-object.ts +13 -0
  87. package/src/middleware/expand-activity-stream.test.data.ts +365 -0
  88. package/src/middleware/expand-activity-stream.test.ts +78 -0
  89. package/src/middleware/expand-activity-stream.ts +27 -0
  90. package/src/middleware/store-credentials.test.ts +73 -0
  91. package/src/middleware/store-credentials.ts +16 -0
  92. package/src/middleware/validate.d.ts +1 -0
  93. package/src/middleware/validate.test.data.ts +261 -0
  94. package/src/middleware/validate.test.ts +83 -0
  95. package/src/middleware/validate.ts +49 -0
  96. package/src/middleware.d.ts +21 -0
  97. package/src/middleware.test.ts +154 -0
  98. package/src/middleware.ts +52 -0
  99. package/src/platform-instance.test.ts +237 -0
  100. package/src/platform-instance.ts +242 -0
  101. package/src/platform.ts +191 -0
  102. package/src/process-manager.ts +64 -0
  103. package/src/routes.test.ts +100 -0
  104. package/src/routes.ts +98 -0
  105. package/src/sockethub.d.ts +1 -0
  106. package/src/sockethub.ts +165 -0
  107. package/src/store.test.ts +28 -0
  108. package/src/store.ts +17 -0
  109. package/test/init-suite.js +41 -0
  110. package/test/queue.functional.test.js +0 -0
  111. package/test/sockethub-suite.js +25 -0
  112. package/tsconfig.json +18 -0
  113. package/views/examples/dummy.ejs +93 -0
  114. package/views/examples/feeds.ejs +90 -0
  115. package/views/examples/irc.ejs +239 -0
  116. package/views/examples/shared.js +72 -0
  117. package/views/examples/xmpp.ejs +191 -0
  118. package/views/index.ejs +17 -0
@@ -0,0 +1,191 @@
1
+ import debug from 'debug';
2
+ import hash from "object-hash";
3
+ import Queue from 'bull';
4
+ import { IActivityStream } from "@sockethub/schemas";
5
+ import { getPlatformId, decryptJobData } from "./common";
6
+ import { JobDataDecrypted, JobEncrypted } from "./sockethub";
7
+ import { MessageFromParent } from './platform-instance';
8
+ import { getSessionStore } from "./store";
9
+
10
+ // command-line params
11
+ const parentId = process.argv[2];
12
+ const platformName = process.argv[3];
13
+ let identifier = process.argv[4];
14
+ const loggerPrefix = `sockethub:platform:${platformName}:${identifier}`;
15
+ let logger = debug(loggerPrefix);
16
+
17
+ const redisConfig = process.env.REDIS_URL ? process.env.REDIS_URL
18
+ : { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT };
19
+ const PlatformModule = require(`@sockethub/platform-${platformName}`);
20
+
21
+ let queueStarted = false;
22
+ let parentSecret1: string, parentSecret2: string;
23
+
24
+ logger(`platform handler initialized for ${platformName} ${identifier}`);
25
+
26
+ export interface PlatformSession {
27
+ debug(msg: string): void;
28
+ sendToClient(msg: IActivityStream, special?: string): void;
29
+ updateActor(credentials: object): void;
30
+ }
31
+
32
+ /**
33
+ * Handle any uncaught errors from the platform by alerting the worker and shutting down.
34
+ */
35
+ process.on('uncaughtException', (err) => {
36
+ console.log('EXCEPTION IN PLATFORM');
37
+ // eslint-disable-next-line security-node/detect-crlf
38
+ console.log(err.stack);
39
+ process.send(['error', err.toString()]);
40
+ process.exit(1);
41
+ });
42
+
43
+ /**
44
+ * Incoming messages from the worker to this platform. Data is an array, the first property is the
45
+ * method to call, the rest are params.
46
+ */
47
+ process.on('message', (data: MessageFromParent) => {
48
+ if (data[0] === 'secrets') {
49
+ parentSecret1 = data[1].parentSecret1;
50
+ parentSecret2 = data[1].parentSecret2;
51
+ startQueueListener();
52
+ }
53
+ });
54
+
55
+
56
+ /**
57
+ * Initialize platform module
58
+ */
59
+ const platformSession: PlatformSession = {
60
+ debug: debug(`sockethub:platform:${platformName}:${identifier}`),
61
+ sendToClient: getSendFunction('message'),
62
+ updateActor: updateActor
63
+ };
64
+ const platform = new PlatformModule(platformSession);
65
+
66
+ /**
67
+ * Get the credentials stored for this user in this sessions store, if given the correct
68
+ * sessionSecret.
69
+ * @param actorId
70
+ * @param sessionId
71
+ * @param sessionSecret
72
+ * @param cb
73
+ */
74
+ function getCredentials(actorId: string, sessionId: string, sessionSecret: string, cb: Function) {
75
+ if (platform.config.noCredentials) { return cb(); }
76
+ const store = getSessionStore(parentId, parentSecret1, sessionId, sessionSecret);
77
+ store.get(actorId, (err, credentials) => {
78
+ if (platform.config.persist) {
79
+ // don't continue if we don't get credentials
80
+ if (err) { return cb(err); }
81
+ } else if (! credentials) {
82
+ // also skip if this is a non-persist platform with no credentials
83
+ return cb();
84
+ }
85
+
86
+ if (platform.credentialsHash) {
87
+ if (platform.credentialsHash !== hash(credentials.object)) {
88
+ return cb('provided credentials do not match existing platform instance for actor '
89
+ + platform.actor.id);
90
+ }
91
+ } else {
92
+ platform.credentialsHash = hash(credentials.object);
93
+ }
94
+ cb(undefined, credentials);
95
+ });
96
+ }
97
+
98
+ /**
99
+ * Returns a function used to handle completed jobs from the platform code (the `done` callback).
100
+ * @param secret the secret used to decrypt credentials
101
+ */
102
+ function getJobHandler(secret: string) {
103
+ return (job: JobEncrypted, done: Function) => {
104
+ const jobData: JobDataDecrypted = decryptJobData(job, secret);
105
+ const jobLog = debug(`${loggerPrefix}:${jobData.sessionId}`);
106
+ jobLog(`received ${jobData.title} ${jobData.msg.type}`);
107
+ const sessionSecret = jobData.msg.sessionSecret;
108
+ delete jobData.msg.sessionSecret;
109
+
110
+ return getCredentials(jobData.msg.actor.id, jobData.sessionId, sessionSecret,
111
+ (err, credentials) => {
112
+ if (err) { return done(new Error(err)); }
113
+ let jobCallbackCalled = false;
114
+ const doneCallback = (err, result) => {
115
+ if (jobCallbackCalled) { return; }
116
+ jobCallbackCalled = true;
117
+ if (err) {
118
+ jobLog(`errored ${jobData.title} ${jobData.msg.type}`);
119
+ let errMsg;
120
+ // some error objects (eg. TimeoutError) don't interoplate correctly to human-readable
121
+ // so we have to do this little dance
122
+ try {
123
+ errMsg = err.toString();
124
+ } catch (e) {
125
+ errMsg = err;
126
+ }
127
+ done(new Error(errMsg));
128
+ } else {
129
+ jobLog(`completed ${jobData.title} ${jobData.msg.type}`);
130
+ done(null, result);
131
+ }
132
+ };
133
+ if ((Array.isArray(platform.config.requireCredentials)) &&
134
+ (platform.config.requireCredentials.includes(jobData.msg.type))) {
135
+ // add the credentials object if this method requires it
136
+ platform[jobData.msg.type](jobData.msg, credentials, doneCallback);
137
+ } else if (platform.config.persist) {
138
+ if (platform.initialized) {
139
+ platform[jobData.msg.type](jobData.msg, doneCallback);
140
+ } else {
141
+ done(new Error(`${jobData.msg.type} called on uninitialized platform`));
142
+ }
143
+ } else {
144
+ platform[jobData.msg.type](jobData.msg, doneCallback);
145
+ }
146
+ });
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Get an function which sends a message to the parent thread (PlatformInstance). The platform
152
+ * can call that function to send messages back to the client.
153
+ * @param command string containing the type of command to be sent. 'message' or 'close'
154
+ */
155
+ function getSendFunction(command: string) {
156
+ return function (msg: IActivityStream, special?: string) {
157
+ process.send([command, msg, special]);
158
+ };
159
+ }
160
+
161
+ /**
162
+ * When a user changes it's actor name, the channel identifier changes, we need to ensure that
163
+ * both the queue thread (listening on the channel for jobs) and the logging object are updated.
164
+ * @param credentials
165
+ */
166
+ function updateActor(credentials) {
167
+ identifier = getPlatformId(platformName, credentials.actor.id);
168
+ logger(`platform actor updated to ${credentials.actor.id} identifier ${identifier}`);
169
+ logger = debug(`sockethub:platform:${identifier}`);
170
+ platform.credentialsHash = hash(credentials.object);
171
+ platform.debug = debug(`sockethub:platform:${platformName}:${identifier}`);
172
+ process.send(['updateActor', undefined, identifier]);
173
+ startQueueListener(true);
174
+ }
175
+
176
+ /**
177
+ * starts listening on the queue for incoming jobs
178
+ * @param refresh boolean if the param is true, we re-init the queue.process
179
+ * (used when identifier changes)
180
+ */
181
+ function startQueueListener(refresh: boolean = false) {
182
+ const secret = parentSecret1 + parentSecret2;
183
+ if ((queueStarted) && (!refresh)) {
184
+ logger('start queue called multiple times, skipping');
185
+ return;
186
+ }
187
+ const queue = new Queue(parentId + identifier, { redis: redisConfig });
188
+ queueStarted = true;
189
+ logger('listening on the queue for incoming jobs');
190
+ queue.process(getJobHandler(secret));
191
+ }
@@ -0,0 +1,64 @@
1
+ import init from './bootstrap/init';
2
+ import PlatformInstance, {
3
+ platformInstances, PlatformInstanceParams, MessageFromParent } from "./platform-instance";
4
+ import { getPlatformId } from "./common";
5
+
6
+ class ProcessManager {
7
+ private readonly parentId: string;
8
+ private readonly parentSecret1: string;
9
+ private readonly parentSecret2: string;
10
+
11
+ constructor(parentId: string, parentSecret1: string, parentSecret2: string) {
12
+ this.parentId = parentId;
13
+ this.parentSecret1 = parentSecret1;
14
+ this.parentSecret2 = parentSecret2;
15
+ }
16
+
17
+ get(platform: string, actorId: string, sessionId?: string): PlatformInstance {
18
+ const platformDetails = init.platforms.get(platform);
19
+ let pi;
20
+
21
+ if (platformDetails.config.persist) {
22
+ // ensure process is started - one for each actor
23
+ pi = this.ensureProcess(platform, sessionId, actorId);
24
+ } else {
25
+ // ensure process is started - one for all jobs
26
+ pi = this.ensureProcess(platform);
27
+ }
28
+ pi.config = platformDetails.config;
29
+ return pi;
30
+ }
31
+
32
+ private createPlatformInstance(identifier: string, platform: string,
33
+ actor?: string): PlatformInstance {
34
+ const secrets: MessageFromParent = [
35
+ 'secrets', {
36
+ parentSecret1: this.parentSecret1,
37
+ parentSecret2: this.parentSecret2
38
+ }
39
+ ];
40
+ const platformInstanceConfig: PlatformInstanceParams = {
41
+ identifier: identifier,
42
+ platform: platform,
43
+ parentId: this.parentId,
44
+ actor: actor
45
+ };
46
+ const platformInstance = new PlatformInstance(platformInstanceConfig);
47
+ platformInstance.initQueue(this.parentSecret1 + this.parentSecret2);
48
+ platformInstance.process.send(secrets);
49
+ return platformInstance;
50
+ }
51
+
52
+ private ensureProcess(platform: string, sessionId?: string, actor?: string): PlatformInstance {
53
+ const identifier = getPlatformId(platform, actor);
54
+ const platformInstance = platformInstances.get(identifier) ||
55
+ this.createPlatformInstance(identifier, platform, actor);
56
+ if (sessionId) {
57
+ platformInstance.registerSession(sessionId);
58
+ }
59
+ platformInstances.set(identifier, platformInstance);
60
+ return platformInstance;
61
+ }
62
+ }
63
+
64
+ export default ProcessManager;
@@ -0,0 +1,100 @@
1
+ import { expect } from 'chai';
2
+ import * as sinon from 'sinon';
3
+ import { existsSync } from "fs";
4
+
5
+ import routes, { basePaths, examplePaths, examplePages, IRoutePaths } from "./routes";
6
+
7
+ describe('routes/base', () => {
8
+
9
+ afterEach(() => {
10
+ sinon.restore();
11
+ });
12
+
13
+ it('can find each of the base files it serves', () => {
14
+ Object.values(basePaths).forEach((fwd: string) => {
15
+ try {
16
+ expect(existsSync(fwd)).to.be.true;
17
+ } catch (e) {
18
+ throw new Error(`Unable to resolve path ${fwd}`);
19
+ }
20
+ });
21
+ });
22
+
23
+ it('can find each of the example files it serves', () => {
24
+ Object.values(examplePaths).forEach((fwd: string) => {
25
+ try {
26
+ expect(existsSync(fwd)).to.be.true;
27
+ } catch (e) {
28
+ throw new Error(`Unable to resolve path ${fwd}`);
29
+ }
30
+ });
31
+ });
32
+
33
+ it('can find each of the example page files it serves', () => {
34
+ Object.values(examplePages).forEach((fwd: string) => {
35
+ try {
36
+ expect(existsSync(fwd)).to.be.true;
37
+ } catch (e) {
38
+ throw new Error(`Unable to resolve path ${fwd}`);
39
+ }
40
+ });
41
+ });
42
+
43
+ it('adds base routes', () => {
44
+ const app = {
45
+ get: sinon.spy()
46
+ };
47
+ routes.setup(app, false);
48
+ sinon.assert.callCount(
49
+ app.get,
50
+ Object.keys(basePaths).length
51
+ );
52
+ });
53
+
54
+ it('adds base and example routes', () => {
55
+ const app = {
56
+ get: sinon.spy()
57
+ };
58
+ routes.setup(app, true);
59
+ sinon.assert.callCount(
60
+ app.get,
61
+ Object.keys(basePaths).length
62
+ + Object.keys(examplePaths).length
63
+ + Object.keys(examplePages).length
64
+ );
65
+ });
66
+
67
+ it('handles calls to base routes as expected', () => {
68
+ let routeHandlers: any = {};
69
+ let app = {
70
+ get: (path: string | number, route: any) => {
71
+ routeHandlers[path] = route;
72
+ }
73
+ };
74
+ routes.setup(app, true);
75
+
76
+ function verifyPathRoutes(pathMap: IRoutePaths) {
77
+ Object.keys(pathMap).forEach((path) => {
78
+ const res = {
79
+ setHeader: sinon.spy(),
80
+ sendFile: sinon.spy()
81
+ };
82
+ expect(pathMap[path].endsWith('.ejs')).to.be.false;
83
+ routeHandlers[path]({url: path}, res);
84
+ sinon.assert.called(res.setHeader);
85
+ sinon.assert.calledWith(res.sendFile, pathMap[path]);
86
+ });
87
+ }
88
+ verifyPathRoutes(basePaths);
89
+ verifyPathRoutes(examplePaths);
90
+
91
+ Object.keys(examplePages).forEach((path) => {
92
+ const res = {
93
+ render: sinon.spy()
94
+ };
95
+ expect(examplePages[path].endsWith('.ejs')).to.be.true;
96
+ routeHandlers[path]({url: path}, res);
97
+ sinon.assert.called(res.render);
98
+ });
99
+ });
100
+ });
package/src/routes.ts ADDED
@@ -0,0 +1,98 @@
1
+ import path from 'path';
2
+ import config from "./config";
3
+ import debug from 'debug';
4
+
5
+ const debug_scope = process.env.DEBUG || '',
6
+ logger = debug('sockethub:server:routes'),
7
+ address = config.get('public:protocol') + '://' +
8
+ config.get('public:host') + ':' +
9
+ config.get('public:port') +
10
+ config.get('public:path');
11
+
12
+ export interface IRoutePaths {
13
+ [key: string]: string;
14
+ }
15
+
16
+ export const basePaths: IRoutePaths = {
17
+ '/sockethub-client.js':
18
+ path.resolve(`${__dirname}/../node_modules/@sockethub/client/dist/sockethub-client.js`),
19
+ '/sockethub-client.min.js':
20
+ path.resolve(`${__dirname}/../node_modules/@sockethub/client/dist/sockethub-client.min.js`),
21
+ '/sockethub-client.js.map':
22
+ path.resolve(`${__dirname}/../node_modules/@sockethub/client/dist/sockethub-client.js.map`),
23
+ '/socket.io.js': path.resolve(`${__dirname}/../node_modules/socket.io/client-dist/socket.io.js`)
24
+ };
25
+
26
+ export const examplePaths: IRoutePaths = {
27
+ '/jquery.js': path.resolve(`${__dirname}/../node_modules/jquery/dist/jquery.min.js`),
28
+ '/jquery.min.map': path.resolve(`${__dirname}/../node_modules/jquery/dist/jquery.min.map`),
29
+ '/examples/shared.js': path.resolve(`${__dirname}/../views/examples/shared.js`)
30
+ };
31
+
32
+ export const examplePages: IRoutePaths = {
33
+ '/': path.resolve(`${__dirname}/../views/index.ejs`),
34
+ '/examples/dummy': path.resolve(`${__dirname}/../views/examples/dummy.ejs`),
35
+ '/examples/feeds': path.resolve(`${__dirname}/../views/examples/feeds.ejs`),
36
+ '/examples/irc': path.resolve(`${__dirname}/../views/examples/irc.ejs`),
37
+ '/examples/xmpp': path.resolve(`${__dirname}/../views/examples/xmpp.ejs`)
38
+ };
39
+
40
+
41
+ function prepFileRoutes(pathMap) {
42
+ let _routes = [];
43
+ Object.keys(pathMap).forEach((key) => {
44
+ _routes.push({
45
+ meta: {
46
+ method: 'GET',
47
+ path: key
48
+ },
49
+ route: (req, res) => {
50
+ logger(`serving resource ${req.url}`);
51
+ res.setHeader('Access-Control-Allow-Origin', '*');
52
+ res.sendFile(pathMap[req.url]);
53
+ }
54
+ });
55
+ });
56
+ return _routes;
57
+ }
58
+ const baseRoutes = prepFileRoutes(basePaths);
59
+ const exampleRoutes = prepFileRoutes(examplePaths);
60
+
61
+
62
+ Object.keys(examplePages).forEach((key) => {
63
+ exampleRoutes.push({
64
+ meta: {
65
+ method: 'GET',
66
+ path: key
67
+ },
68
+ route: (req, res) => {
69
+ logger(`serving page ${req.url}`);
70
+ res.render(examplePages[req.url], {
71
+ debug_scope: debug_scope,
72
+ address: address,
73
+ });
74
+ }
75
+ });
76
+ });
77
+
78
+ function addRoute(app) {
79
+ return (route) => {
80
+ app[route.meta.method.toLowerCase()](
81
+ route.meta.path,
82
+ route.route
83
+ );
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Setup
89
+ */
90
+ const routes = {
91
+ setup: function (app: any, examplesEnabled: boolean = config.get('examples:enabled')) {
92
+ baseRoutes.forEach(addRoute(app));
93
+ if (examplesEnabled) {
94
+ exampleRoutes.forEach(addRoute(app));
95
+ }
96
+ }
97
+ };
98
+ export default routes;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,165 @@
1
+ import debug from 'debug';
2
+ import { Socket } from "socket.io";
3
+ import { IActivityStream } from "@sockethub/schemas";
4
+
5
+ import crypto from './crypto';
6
+ import init from './bootstrap/init';
7
+ import middleware from './middleware';
8
+ import createActivityObject from "./middleware/create-activity-object";
9
+ import expandActivityStream from "./middleware/expand-activity-stream";
10
+ import storeCredentials from "./middleware/store-credentials";
11
+ import validate from "./middleware/validate";
12
+ import janitor from './janitor';
13
+ import listener from './listener';
14
+ import ProcessManager from "./process-manager";
15
+ import PlatformInstance, { platformInstances } from "./platform-instance";
16
+ import { getSessionStore } from "./store";
17
+
18
+ const log = debug('sockethub:server:core');
19
+
20
+
21
+ export interface JobDataDecrypted {
22
+ title?: string;
23
+ msg: IActivityStream;
24
+ sessionId: string;
25
+ }
26
+
27
+ export interface JobDataEncrypted {
28
+ title?: string;
29
+ msg: string;
30
+ sessionId: string;
31
+ }
32
+
33
+ export interface JobDecrypted {
34
+ data: JobDataDecrypted,
35
+ remove?: Function;
36
+ }
37
+
38
+ export interface JobEncrypted {
39
+ data: JobDataEncrypted,
40
+ remove?: Function;
41
+ }
42
+
43
+ function attachError(err, msg) {
44
+ if (typeof msg !== 'object') {
45
+ msg = { context: 'error' };
46
+ }
47
+ msg.error = err.toString();
48
+ delete msg.sessionSecret;
49
+ return msg;
50
+ }
51
+
52
+ class Sockethub {
53
+ private readonly parentId: string;
54
+ private readonly parentSecret1: string;
55
+ private readonly parentSecret2: string;
56
+ counter: number;
57
+ platforms: Map<string, object>;
58
+ status: boolean;
59
+ queue: any;
60
+ processManager: ProcessManager;
61
+
62
+ constructor() {
63
+ this.counter = 0;
64
+ this.platforms = init.platforms;
65
+ this.status = false;
66
+ this.parentId = crypto.randToken(16);
67
+ this.parentSecret1 = crypto.randToken(16);
68
+ this.parentSecret2 = crypto.randToken(16);
69
+ this.processManager = new ProcessManager(
70
+ this.parentId, this.parentSecret1, this.parentSecret2);
71
+ log('session id: ' + this.parentId);
72
+ }
73
+
74
+ /**
75
+ * initialization of Sockethub starts here
76
+ */
77
+ boot() {
78
+ if (this.status) {
79
+ return log('Sockethub.boot() called more than once');
80
+ } else {
81
+ this.status = true;
82
+ }
83
+
84
+ log('active platforms: ', [...init.platforms.keys()]);
85
+ janitor.clean(); // start cleanup cycle
86
+ listener.start(); // start external services
87
+ log('registering handlers');
88
+ listener.io.on('connection', this.handleIncomingConnection.bind(this));
89
+ }
90
+
91
+ async removeAllPlatformInstances() {
92
+ for (let platform of platformInstances) {
93
+ await platform[1].destroy();
94
+ }
95
+ }
96
+
97
+ private createJob(socketId: string, platformInstance: PlatformInstance, msg): JobDataEncrypted {
98
+ const title = `${msg.context}-${(msg.id) ? msg.id : this.counter++}`;
99
+ const job: JobDataEncrypted = {
100
+ title: title,
101
+ sessionId: socketId,
102
+ msg: crypto.encrypt(msg, this.parentSecret1 + this.parentSecret2)
103
+ };
104
+ return job;
105
+ };
106
+
107
+ private handleIncomingConnection(socket: Socket) {
108
+ // session-specific debug messages
109
+ const sessionLog = debug('sockethub:server:core:' + socket.id),
110
+ sessionSecret = crypto.randToken(16),
111
+ // store instance is session-specific
112
+ store = getSessionStore(this.parentId, this.parentSecret1, socket.id, sessionSecret);
113
+
114
+ sessionLog(`socket.io connection`);
115
+
116
+ socket.on('disconnect', () => {
117
+ sessionLog('disconnect received from client');
118
+ });
119
+
120
+ socket.on('credentials',
121
+ middleware('credentials')
122
+ .use(expandActivityStream)
123
+ .use(validate('credentials', socket.id))
124
+ .use(storeCredentials(store, sessionLog))
125
+ .use((err, data, next) => {
126
+ // error handler
127
+ next(attachError(err, data));
128
+ }).use((data, next) => { next(); })
129
+ .done());
130
+
131
+ // when new activity objects are created on the client side, an event is
132
+ // fired and we receive a copy on the server side.
133
+ socket.on('activity-object',
134
+ middleware('activity-object')
135
+ .use(validate('activity-object', socket.id))
136
+ .use(createActivityObject)
137
+ .use((err, data, next) => {
138
+ next(attachError(err, data));
139
+ }).use((data, next) => { next(); })
140
+ .done());
141
+
142
+ socket.on('message',
143
+ middleware('message')
144
+ .use(expandActivityStream)
145
+ .use(validate('message', socket.id))
146
+ .use((msg, next) => {
147
+ // The platform thread must find the credentials on their own using the given
148
+ // sessionSecret, which indicates that this specific session (socket
149
+ // connection) has provided credentials.
150
+ msg.sessionSecret = sessionSecret;
151
+ next(msg);
152
+ }).use((err, data, next) => {
153
+ next(attachError(err, data));
154
+ }).use((msg, next) => {
155
+ const platformInstance = this.processManager.get(msg.context, msg.actor.id, socket.id);
156
+ sessionLog(`queued to channel ${platformInstance.id}`);
157
+ const job = this.createJob(socket.id, platformInstance, msg);
158
+ // job validated and queued, store socket.io callback for when job is completed
159
+ platformInstance.completedJobHandlers.set(job.title, next);
160
+ platformInstance.queue.add(job);
161
+ }).done());
162
+ }
163
+ }
164
+
165
+ export default Sockethub;
@@ -0,0 +1,28 @@
1
+ import proxyquire from 'proxyquire';
2
+ import { expect } from 'chai';
3
+ import * as sinon from 'sinon';
4
+
5
+ proxyquire.noPreserveCache();
6
+ proxyquire.noCallThru();
7
+
8
+ const MockSecureStore = sinon.fake();
9
+
10
+ const StoreMod = proxyquire('./store', {
11
+ 'secure-store-redis': MockSecureStore
12
+ });
13
+
14
+ const getSessionStore = StoreMod.getSessionStore;
15
+
16
+ describe('getSessionStore', () => {
17
+ it('returns a valid Store object', () => {
18
+ const store = getSessionStore('a parent id', 'a parent secret',
19
+ 'a session id', 'a session secret');
20
+ sinon.assert.calledOnce(MockSecureStore);
21
+ sinon.assert.calledWith(MockSecureStore, {
22
+ namespace: 'sockethub:a parent id:session:a session id:store',
23
+ secret: 'a parent secreta session secret',
24
+ redis: { host: '127.0.0.1', port: 6379 }
25
+ });
26
+ expect(typeof store).to.equal('object');
27
+ });
28
+ });
package/src/store.ts ADDED
@@ -0,0 +1,17 @@
1
+ import SecureStore from 'secure-store-redis';
2
+
3
+ import config from "./config";
4
+
5
+ export interface ISecureStoreInstance {
6
+ save(id: string, obj: any, cb: Function);
7
+ get(id: string, cb: Function);
8
+ }
9
+
10
+ export function getSessionStore(parentId: string, parentSecret: string,
11
+ sessionId: string, sessionSecret: string): ISecureStoreInstance {
12
+ return new SecureStore({
13
+ namespace: 'sockethub:' + parentId + ':session:' + sessionId + ':store',
14
+ secret: parentSecret + sessionSecret,
15
+ redis: config.get('redis')
16
+ });
17
+ }