Skip to content

Commit

Permalink
feat: filter out non-JSON API resources
Browse files Browse the repository at this point in the history
Mateu Aguiló Bosch committed Jun 17, 2018

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent ea982dd commit e44014e
Showing 12 changed files with 114 additions and 13 deletions.
1 change: 1 addition & 0 deletions .emdaer/docs/notes.md
Original file line number Diff line number Diff line change
@@ -14,3 +14,4 @@ just meant to save ideas for documentation to process some other time._
- Create @contentacms/… submodules for logging interfaces like Splunk.
- Create a @contentacms/redisShare submodule for a shared Redis server between
Drupal and node.
- Forward requests to /jsonrpc
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@
-->

<!--
emdaerHash:fd626352cb7f595c5f3c47cc6b373f5c
emdaerHash:766f7f04de1a95368ac0e89e88166413
-->

<h1 id="contentajs-img-align-right-src-logo-svg-alt-contenta-logo-title-contenta-logo-width-100-">ContentaJS <img align="right" src="./logo.svg" alt="Contenta logo" title="Contenta logo" width="100"></h1>
@@ -119,6 +119,7 @@ just meant to save ideas for documentation to process some other time.</em></p>
<li>Create @contentacms/… submodules for logging interfaces like Splunk.</li>
<li>Create a @contentacms/redisShare submodule for a shared Redis server between
Drupal and node.</li>
<li>Forward requests to /jsonrpc</li>
</ul>
<h2 id="contributors">Contributors</h2>
<details>
12 changes: 11 additions & 1 deletion src/bootstrap.test.js
Original file line number Diff line number Diff line change
@@ -7,7 +7,17 @@ jest
.mockImplementation(() => ({ process: { pid: 42 } }));
jest.mock('./helpers/fetchCmsMeta', () => () =>
Promise.resolve([
[{ jsonApiPrefix: 'prefix' }, { result: { prefix: 'myPrefix' } }],
[
{ jsonApiPrefix: 'prefix' },
{
result: {
openApi: {
basePath: '/myPrefix',
paths: ['/foo', '/foo/{bar}', '/foo/{bar}/oof/{baz}'],
},
},
},
],
])
);
jest.spyOn(Adios.master, 'init').mockImplementation();
5 changes: 3 additions & 2 deletions src/helpers/app.js
Original file line number Diff line number Diff line change
@@ -23,7 +23,8 @@ app.disable('x-powered-by');
// Enable etags.
app.enable('etag');
app.set('etag', 'strong');
const jsonApiPrefix = _.get(process, 'env.jsonApiPrefix');
const jsonApiPrefix = _.get(process, 'env.jsonApiPrefix', '/jsonapi');
const jsonApiPaths = JSON.parse(_.get(process, 'env.jsonApiPaths', '[]'));
const cmsHost = config.get('cms.host');

const corsHandler = cors(config.util.toObject(config.get('cors')));
@@ -32,7 +33,7 @@ app.use(corsHandler);
app.options('*', corsHandler);

// Initialize the request object with valuable information.
app.use(copyToRequestObject({ jsonApiPrefix, cmsHost }));
app.use(copyToRequestObject({ jsonApiPrefix, jsonApiPaths, cmsHost }));

// Healthcheck is a special endpoint used for application monitoring.
app.get('/healthcheck', healthcheck);
3 changes: 2 additions & 1 deletion src/helpers/cmsMeta/plugins/jsonapi/plugnplay.yml
Original file line number Diff line number Diff line change
@@ -8,4 +8,5 @@ rpcMethod: jsonapi.metadata
resultMap:
# The name of the node.js environment variable: The path in the JSON RPC's
# result object.
jsonApiPrefix: openApi.basePath
jsonApiPrefix: basePath
jsonApiPaths: paths
28 changes: 22 additions & 6 deletions src/helpers/fetchCmsMeta.js
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ const logger = require('pino')();
const cmsHost = config.get('cms.host');

const jsonrpc = require('./jsonrpc')(cmsHost);

const openApiPathToRegExp = require('./openApiPathToRegExp');
/**
* Connects to the CMS to get some important bootstrap information.
*
@@ -46,11 +46,27 @@ module.exports = (): Promise<Array<[ObjectLiteral, JsonRpcResponseItem]>> => {
.then((plugin: PluginInstance) =>
Promise.all([
plugin.descriptor.resultMap,
plugin.exports.fetch().catch(error => {
// If a particular fetcher returns an error, log it then swallow.
logger.error(error);
return error;
}),
plugin.exports
.fetch()
.then(res => {
// Contenta CMS will send the paths as the Open API
// specification, we need them to match incoming requests
// so we transform them into regular expressions.
const paths = openApiPathToRegExp(
Object.keys(res.result.openApi.paths)
);
return {
result: {
basePath: res.result.openApi.basePath,
paths: JSON.stringify(paths),
},
};
})
.catch(error => {
// If a particular fetcher returns an error, log it then swallow.
logger.error(error);
return error;
}),
])
)
)
11 changes: 10 additions & 1 deletion src/helpers/fetchCmsMeta.test.js
Original file line number Diff line number Diff line change
@@ -23,7 +23,16 @@ describe('The metadata bootstrap process', () => {
test('It requests the correct data', () => {
expect.assertions(1);
const jsonrpc = require('./jsonrpc');
jsonrpc.execute.mockImplementationOnce(() => Promise.resolve());
jsonrpc.execute.mockImplementationOnce(() =>
Promise.resolve({
result: {
openApi: {
basePath: '/foo',
paths: { lorem: 'ipsum' },
},
},
})
);
return fetchCmsMeta().then(() => {
expect(jsonrpc.execute).toHaveBeenCalledWith({
id: 'req-jsonapi.metadata',
2 changes: 1 addition & 1 deletion src/helpers/jsonrpc.test.js
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ jest.mock('./got', () =>
case 'foo/jsonrpc/methods':
return resFromObj({ data: [{ id: 'lorem' }, { id: 'broken' }] });
case 'foo/jsonrpc':
switch (options.body.method) {
switch (options.query.query.method) {
case 'lorem':
return resFromObj({ result: { foo: 'bar' } });
case 'broken':
15 changes: 15 additions & 0 deletions src/helpers/openApiPathToRegExp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// @flow

/**
* Takes a list of paths in Open API format and makes them into regexps.
*
* @param {string[]} paths
* The Open API paths.
*
* @return {string[]}
* The list of strings ready to feed a RegExp.
*/
module.exports = (paths: Array<string>): Array<string> =>
paths
.map(p => p.replace('/', '\\/').replace(/{[^{}/]+}/g, '[^\\/]+'))
.map(p => `^${p}/?$`);
15 changes: 15 additions & 0 deletions src/helpers/openApiPathToRegExp.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const openApiPathToRegExp = require('./openApiPathToRegExp');

describe('openApiPathToRegExp', () => {
test('It can transform paths', () => {
expect.assertions(1);
const paths = ['/foo', '/foo/{bar}', '/foo/{bar}/oof/{baz}'];
const actual = openApiPathToRegExp(paths);
const expected = [
'^\\/foo/?$',
'^\\/foo/[^\\/]+/?$',
'^\\/foo/[^\\/]+/oof/[^\\/]+/?$',
];
expect(actual).toEqual(expected);
});
});
12 changes: 12 additions & 0 deletions src/routes/proxyHandler.js
Original file line number Diff line number Diff line change
@@ -21,6 +21,18 @@ const errorHandler = require('../middlewares/errorHandler');
*/
module.exports = (req: Request, res: Response, next: NextFunction): void => {
const options = {
// We have a list of the JSON API resources available in Contenta CMS. This
// list is a list of regular expressions that can match any path a resource,
// taking variables into account. Filter the requests that are for
// non-existing resources.
filter(rq) {
// Extract the path part, without query string, of the current request.
const parsed = url.parse(rq.url);
// Return false if it doesn't apply any regular expression path.
return !!rq.jsonApiPaths.find(p =>
new RegExp(p).test(parsed.pathname || '')
);
},
proxyReqPathResolver(rq) {
const thePath: string = _.get(url.parse(rq.url), 'path', '');
return `${req.jsonApiPrefix}${thePath}`;
20 changes: 20 additions & 0 deletions src/routes/proxyHandler.test.js
Original file line number Diff line number Diff line change
@@ -41,6 +41,7 @@ describe('The fallback to CMS', () => {
expect(next).not.toHaveBeenCalled();
expect(proxy.mock.calls[0][0]).toBe('foo');
expect(Object.keys(proxy.mock.calls[0][1])).toEqual([
'filter',
'proxyReqPathResolver',
'proxyReqBodyDecorator',
'proxyErrorHandler',
@@ -101,4 +102,23 @@ describe('The fallback to CMS', () => {
foo: 'bar',
});
});

test('the filter', () => {
expect.assertions(1);
proxyHandler(req, res);
const { filter } = proxy.mock.calls[0][1];
const actual = filter(
{ url: 'https://example.org/lorem', jsonApiPaths: ['/lorem/?'] },
req
);
expect(actual).toBe(true);
});

test('the filter with an empty path', () => {
expect.assertions(1);
proxyHandler(req, res);
const { filter } = proxy.mock.calls[0][1];
const actual = filter({ url: '', jsonApiPaths: ['/lorem/?'] }, req);
expect(actual).toBe(false);
});
});

0 comments on commit e44014e

Please sign in to comment.