ti2 1.0.97 → 1.0.98
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.
- package/controllers/__tests__/cronjobs.js +17 -20
- package/index.js +6 -1
- package/lib/__tests__/sslConfig.js +112 -0
- package/lib/sslConfig.js +63 -0
- package/package.json +1 -1
- package/tutorials/setup-your-instance.md +20 -0
- package/worker/index.js +4 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* globals beforeAll describe it expect */
|
|
2
2
|
const yaml = require('js-yaml');
|
|
3
|
-
const fs = require('fs')
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const R = require('ramda')
|
|
4
5
|
|
|
5
6
|
// Load environment variables and API schema
|
|
6
7
|
const { env: { adminKey } } = process;
|
|
@@ -315,28 +316,24 @@ describe('scheduled cronjob execution', () => {
|
|
|
315
316
|
const url = require('url');
|
|
316
317
|
|
|
317
318
|
server = http.createServer((req, res) => {
|
|
318
|
-
let
|
|
319
|
-
req.on('data', chunk =>
|
|
319
|
+
let callbackPayload = '';
|
|
320
|
+
req.on('data', chunk => callbackPayload += chunk);
|
|
320
321
|
req.on('end', () => {
|
|
321
322
|
const requestUrl = url.parse(req.url, true);
|
|
322
323
|
const receivedUniqueId = requestUrl.query.date;
|
|
324
|
+
const receivedJobId = R.path(['result', 'bullJobId'], JSON.parse(callbackPayload))
|
|
323
325
|
|
|
324
326
|
if (receivedUniqueId === uniqueId) {
|
|
325
327
|
// Check if response and response.bullJobId are available
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
resolve({
|
|
331
|
-
}
|
|
332
|
-
// This case handles if callback arrives but job creation might have failed or response not processed
|
|
333
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
334
|
-
res.end(JSON.stringify({ success: false, message: 'Callback received but job details unavailable.' }));
|
|
335
|
-
reject(new Error('Callback processed but original job details (response.bullJobId) were missing.'));
|
|
336
|
-
}
|
|
328
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
329
|
+
res.end(JSON.stringify({ success: true }));
|
|
330
|
+
// Close the server after sending the response
|
|
331
|
+
server.close(() => {
|
|
332
|
+
resolve({ receivedJobId });
|
|
333
|
+
});
|
|
337
334
|
} else {
|
|
338
335
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
339
|
-
res.end(JSON.stringify({ success: false, message: 'Callback ID mismatch' }));
|
|
336
|
+
return res.end(JSON.stringify({ success: false, message: 'Callback ID mismatch' }));
|
|
340
337
|
// Do not resolve or reject here, let the test timeout if correct callback never comes.
|
|
341
338
|
// Or, optionally, reject:
|
|
342
339
|
// reject(new Error('Callback ID mismatch.'));
|
|
@@ -366,12 +363,12 @@ describe('scheduled cronjob execution', () => {
|
|
|
366
363
|
});
|
|
367
364
|
|
|
368
365
|
expect(response.bullJobId).toBeTruthy();
|
|
366
|
+
bullJobId = response.bullJobId;
|
|
369
367
|
expect(response.id).toBeTruthy();
|
|
370
368
|
cronJobIdToCleanup = response.id;
|
|
371
369
|
|
|
372
|
-
const
|
|
373
|
-
expect(
|
|
374
|
-
bullJobId = response.bullJobId;
|
|
370
|
+
const { receivedJobId } = await callBackServerPromise;
|
|
371
|
+
expect(receivedJobId).toBe(bullJobId);
|
|
375
372
|
|
|
376
373
|
}, 120e3); // Timeout for the cronjob to be executed
|
|
377
374
|
|
|
@@ -380,7 +377,7 @@ describe('scheduled cronjob execution', () => {
|
|
|
380
377
|
expect(bullJobId).toBeTruthy();
|
|
381
378
|
const { queue } = require('../../worker/queue');
|
|
382
379
|
let jobInQueue;
|
|
383
|
-
const maxRetries =
|
|
380
|
+
const maxRetries = 120;
|
|
384
381
|
const retryInterval = 1e3;
|
|
385
382
|
|
|
386
383
|
for (let i = 0; i < maxRetries; i++) {
|
|
@@ -389,6 +386,6 @@ describe('scheduled cronjob execution', () => {
|
|
|
389
386
|
await new Promise(resolveDelay => setTimeout(resolveDelay, retryInterval));
|
|
390
387
|
}
|
|
391
388
|
expect(jobInQueue).toBeNull();
|
|
392
|
-
},
|
|
389
|
+
}, 120e3) // timeout for bull to remove the queue Job once executed (removeOnComplete)
|
|
393
390
|
});
|
|
394
391
|
});
|
package/index.js
CHANGED
|
@@ -15,6 +15,7 @@ const EventEmitter = require('eventemitter2');
|
|
|
15
15
|
const axios = require('axios');
|
|
16
16
|
const curlirize = require('axios-curlirize');
|
|
17
17
|
const { addJob } = require('./worker/queue');
|
|
18
|
+
const { configureAxiosSSL } = require('./lib/sslConfig');
|
|
18
19
|
|
|
19
20
|
const cacheSettings = {
|
|
20
21
|
'*': [
|
|
@@ -78,6 +79,10 @@ module.exports = async ({
|
|
|
78
79
|
const nuName = attr.replace(/_/g, '-').replace(`ti2-${pluginName}-`, '');
|
|
79
80
|
params[nuName] = value;
|
|
80
81
|
});
|
|
82
|
+
// Create a new axios instance for this plugin and configure SSL if needed
|
|
83
|
+
const pluginAxios = axios.create();
|
|
84
|
+
configureAxiosSSL(pluginAxios, process.env.SSL_INSECURE_ALLOWED_DOMAINS);
|
|
85
|
+
|
|
81
86
|
const pluginInstance = await new Plugin({
|
|
82
87
|
cache: {
|
|
83
88
|
drop: args => cache.drop({ ...args, pluginName }),
|
|
@@ -85,7 +90,7 @@ module.exports = async ({
|
|
|
85
90
|
getOrExec: args => cache.getOrExec({ ...args, pluginName }),
|
|
86
91
|
save: args => cache.save({ ...args, pluginName }),
|
|
87
92
|
},
|
|
88
|
-
axios,
|
|
93
|
+
axios: pluginAxios,
|
|
89
94
|
events: ti2Events,
|
|
90
95
|
name: pluginName,
|
|
91
96
|
...params,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const { configureAxiosSSL, isDomainAllowedInsecure } = require('../sslConfig');
|
|
3
|
+
|
|
4
|
+
describe('SSL Configuration', () => {
|
|
5
|
+
describe('isDomainAllowedInsecure', () => {
|
|
6
|
+
it('should return false when no domains are allowed', () => {
|
|
7
|
+
expect(isDomainAllowedInsecure('https://example.com', '')).toBe(false);
|
|
8
|
+
expect(isDomainAllowedInsecure('https://example.com', null)).toBe(false);
|
|
9
|
+
expect(isDomainAllowedInsecure('https://example.com', undefined)).toBe(false);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should match exact domain', () => {
|
|
13
|
+
const allowedDomains = 'localhost|staging.example.com';
|
|
14
|
+
expect(isDomainAllowedInsecure('https://localhost/api', allowedDomains)).toBe(true);
|
|
15
|
+
expect(isDomainAllowedInsecure('https://staging.example.com/api', allowedDomains)).toBe(true);
|
|
16
|
+
expect(isDomainAllowedInsecure('https://production.example.com/api', allowedDomains)).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should match subdomain', () => {
|
|
20
|
+
const allowedDomains = 'example.com';
|
|
21
|
+
expect(isDomainAllowedInsecure('https://api.example.com/endpoint', allowedDomains)).toBe(true);
|
|
22
|
+
expect(isDomainAllowedInsecure('https://staging.api.example.com/endpoint', allowedDomains)).toBe(true);
|
|
23
|
+
expect(isDomainAllowedInsecure('https://example.com/endpoint', allowedDomains)).toBe(true);
|
|
24
|
+
expect(isDomainAllowedInsecure('https://different.com/endpoint', allowedDomains)).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should handle IP addresses', () => {
|
|
28
|
+
const allowedDomains = '192.168.1.100|10.0.0.1';
|
|
29
|
+
expect(isDomainAllowedInsecure('https://192.168.1.100:8080/api', allowedDomains)).toBe(true);
|
|
30
|
+
expect(isDomainAllowedInsecure('https://10.0.0.1/api', allowedDomains)).toBe(true);
|
|
31
|
+
expect(isDomainAllowedInsecure('https://192.168.1.101/api', allowedDomains)).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should be case insensitive', () => {
|
|
35
|
+
const allowedDomains = 'EXAMPLE.COM|LocalHost';
|
|
36
|
+
expect(isDomainAllowedInsecure('https://example.com/api', allowedDomains)).toBe(true);
|
|
37
|
+
expect(isDomainAllowedInsecure('https://EXAMPLE.COM/api', allowedDomains)).toBe(true);
|
|
38
|
+
expect(isDomainAllowedInsecure('https://localhost/api', allowedDomains)).toBe(true);
|
|
39
|
+
expect(isDomainAllowedInsecure('https://LOCALHOST/api', allowedDomains)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should handle invalid URLs gracefully', () => {
|
|
43
|
+
const allowedDomains = 'example.com';
|
|
44
|
+
expect(isDomainAllowedInsecure('not-a-url', allowedDomains)).toBe(false);
|
|
45
|
+
expect(isDomainAllowedInsecure('', allowedDomains)).toBe(false);
|
|
46
|
+
expect(isDomainAllowedInsecure(null, allowedDomains)).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should handle whitespace in domain list', () => {
|
|
50
|
+
const allowedDomains = ' localhost | staging.example.com | 192.168.1.100 ';
|
|
51
|
+
expect(isDomainAllowedInsecure('https://localhost/api', allowedDomains)).toBe(true);
|
|
52
|
+
expect(isDomainAllowedInsecure('https://staging.example.com/api', allowedDomains)).toBe(true);
|
|
53
|
+
expect(isDomainAllowedInsecure('https://192.168.1.100/api', allowedDomains)).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('configureAxiosSSL', () => {
|
|
58
|
+
it('should not modify axios when no domains are configured', () => {
|
|
59
|
+
const axiosInstance = axios.create();
|
|
60
|
+
const originalInterceptors = axiosInstance.interceptors.request.handlers.length;
|
|
61
|
+
|
|
62
|
+
configureAxiosSSL(axiosInstance, '');
|
|
63
|
+
expect(axiosInstance.interceptors.request.handlers.length).toBe(originalInterceptors);
|
|
64
|
+
|
|
65
|
+
configureAxiosSSL(axiosInstance, null);
|
|
66
|
+
expect(axiosInstance.interceptors.request.handlers.length).toBe(originalInterceptors);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should add request interceptor when domains are configured', () => {
|
|
70
|
+
const axiosInstance = axios.create();
|
|
71
|
+
const originalInterceptors = axiosInstance.interceptors.request.handlers.length;
|
|
72
|
+
|
|
73
|
+
configureAxiosSSL(axiosInstance, 'localhost|example.com');
|
|
74
|
+
expect(axiosInstance.interceptors.request.handlers.length).toBe(originalInterceptors + 1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should set httpsAgent for allowed domains', async () => {
|
|
78
|
+
const axiosInstance = axios.create();
|
|
79
|
+
configureAxiosSSL(axiosInstance, 'localhost|staging.example.com');
|
|
80
|
+
|
|
81
|
+
// Test the interceptor by creating a mock config
|
|
82
|
+
const config = { url: 'https://localhost/api' };
|
|
83
|
+
const interceptor = axiosInstance.interceptors.request.handlers[0];
|
|
84
|
+
|
|
85
|
+
if (interceptor && interceptor.fulfilled) {
|
|
86
|
+
const modifiedConfig = await interceptor.fulfilled(config);
|
|
87
|
+
expect(modifiedConfig.httpsAgent).toBeDefined();
|
|
88
|
+
expect(modifiedConfig.httpsAgent.options.rejectUnauthorized).toBe(false);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should not set httpsAgent for non-allowed domains', async () => {
|
|
93
|
+
const axiosInstance = axios.create();
|
|
94
|
+
configureAxiosSSL(axiosInstance, 'localhost|staging.example.com');
|
|
95
|
+
|
|
96
|
+
// Test the interceptor by creating a mock config
|
|
97
|
+
const config = { url: 'https://production.example.com/api' };
|
|
98
|
+
const interceptor = axiosInstance.interceptors.request.handlers[0];
|
|
99
|
+
|
|
100
|
+
if (interceptor && interceptor.fulfilled) {
|
|
101
|
+
const modifiedConfig = await interceptor.fulfilled(config);
|
|
102
|
+
expect(modifiedConfig.httpsAgent).toBeUndefined();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should return the same axios instance', () => {
|
|
107
|
+
const axiosInstance = axios.create();
|
|
108
|
+
const result = configureAxiosSSL(axiosInstance, 'localhost');
|
|
109
|
+
expect(result).toBe(axiosInstance);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
package/lib/sslConfig.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const { URL } = require('url');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates an HTTPS agent with SSL verification disabled
|
|
6
|
+
*/
|
|
7
|
+
const createInsecureAgent = () => new https.Agent({
|
|
8
|
+
rejectUnauthorized: false,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Checks if a URL's domain is in the allowed insecure domains list
|
|
13
|
+
* @param {string} url - The URL to check
|
|
14
|
+
* @param {string} allowedDomains - Pipe-separated list of allowed domains
|
|
15
|
+
* @returns {boolean} True if the domain is allowed to skip SSL verification
|
|
16
|
+
*/
|
|
17
|
+
const isDomainAllowedInsecure = (url, allowedDomains) => {
|
|
18
|
+
if (!allowedDomains || !url) return false;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const parsedUrl = new URL(url);
|
|
22
|
+
const hostname = parsedUrl.hostname.toLowerCase();
|
|
23
|
+
const domains = allowedDomains.split('|').map(d => d.trim().toLowerCase()).filter(Boolean);
|
|
24
|
+
|
|
25
|
+
return domains.some(domain => {
|
|
26
|
+
// Exact match or subdomain match
|
|
27
|
+
return hostname === domain || hostname.endsWith(`.${domain}`);
|
|
28
|
+
});
|
|
29
|
+
} catch (error) {
|
|
30
|
+
// Invalid URL, don't allow insecure
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Configures an axios instance to allow insecure SSL for specific domains
|
|
37
|
+
* @param {Object} axiosInstance - The axios instance to configure
|
|
38
|
+
* @param {string} allowedDomains - Pipe-separated list of allowed domains (from env var)
|
|
39
|
+
* @returns {Object} The configured axios instance
|
|
40
|
+
*/
|
|
41
|
+
const configureAxiosSSL = (axiosInstance, allowedDomains) => {
|
|
42
|
+
if (!allowedDomains) return axiosInstance;
|
|
43
|
+
|
|
44
|
+
const insecureAgent = createInsecureAgent();
|
|
45
|
+
|
|
46
|
+
// Add request interceptor to conditionally apply insecure agent
|
|
47
|
+
axiosInstance.interceptors.request.use(
|
|
48
|
+
(config) => {
|
|
49
|
+
if (config.url && isDomainAllowedInsecure(config.url, allowedDomains)) {
|
|
50
|
+
config.httpsAgent = insecureAgent;
|
|
51
|
+
}
|
|
52
|
+
return config;
|
|
53
|
+
},
|
|
54
|
+
(error) => Promise.reject(error)
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
return axiosInstance;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
module.exports = {
|
|
61
|
+
configureAxiosSSL,
|
|
62
|
+
isDomainAllowedInsecure,
|
|
63
|
+
};
|
package/package.json
CHANGED
|
@@ -29,6 +29,7 @@ A MySQl instance is required and the following environment variables are require
|
|
|
29
29
|
- jwtKey (a key to encrypt user sessions request)
|
|
30
30
|
- frontendKey (an additional optional key to validate the origin of the requests)
|
|
31
31
|
- PORT (an optional port to run the http server, in case a port is not specified as a parameter)
|
|
32
|
+
- SSL_INSECURE_ALLOWED_DOMAINS (optional, pipe-separated list of domains where SSL certificate verification is skipped)
|
|
32
33
|
|
|
33
34
|
```
|
|
34
35
|
DB_URL=mysql://root:@mysqlserver/ti2development
|
|
@@ -39,6 +40,25 @@ frontendKey=IpOiLahrGQxsQimwoK7Z
|
|
|
39
40
|
adminKey=bCviyz7iKyuEBoOVlODn
|
|
40
41
|
```
|
|
41
42
|
|
|
43
|
+
#### SSL Configuration for Development/Testing
|
|
44
|
+
|
|
45
|
+
For development or testing environments where you need to connect to APIs with self-signed or invalid SSL certificates, you can use the `SSL_INSECURE_ALLOWED_DOMAINS` environment variable:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Skip SSL verification for specific domains
|
|
49
|
+
SSL_INSECURE_ALLOWED_DOMAINS="localhost|staging.api.com|192.168.1.100"
|
|
50
|
+
|
|
51
|
+
# Multiple domains separated by pipe
|
|
52
|
+
SSL_INSECURE_ALLOWED_DOMAINS="dev.example.com|test.example.com"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This configuration will:
|
|
56
|
+
- Skip SSL certificate verification for the specified domains
|
|
57
|
+
- Apply only to plugin axios instances (external API calls)
|
|
58
|
+
- Maintain normal SSL verification for all other domains
|
|
59
|
+
|
|
60
|
+
**⚠️ Warning**: Only use this in development/testing environments. Never skip SSL verification in production for security reasons.
|
|
61
|
+
|
|
42
62
|
### Initialize a new NodeJS project
|
|
43
63
|
|
|
44
64
|
We suggest creating a folder with your instance name+i2 (i.e acme-ti2); then initialize an node's npm package and install ti2 along with any plugins to be used.
|
package/worker/index.js
CHANGED
|
@@ -4,6 +4,10 @@ const R = require('ramda');
|
|
|
4
4
|
const { queue, saveResult } = require('./queue');
|
|
5
5
|
const bookingsControllerFactory = require('../controllers/bookings');
|
|
6
6
|
const mainAxios = require('axios'); // Generic axios instance for direct calls
|
|
7
|
+
const { configureAxiosSSL } = require('../lib/sslConfig');
|
|
8
|
+
|
|
9
|
+
// Configure SSL for the main axios instance
|
|
10
|
+
configureAxiosSSL(mainAxios, process.env.SSL_INSECURE_ALLOWED_DOMAINS);
|
|
7
11
|
|
|
8
12
|
const workers = process.env.WEB_CONCURRENCY || 2;
|
|
9
13
|
const maxJobsPerWorker = process.env.MAX_JOBS_PER_WORKER || 1;
|