Skip to content

Commit

Permalink
feat: filter out non-JSON API resources
Browse files Browse the repository at this point in the history
  • Loading branch information
Mateu Aguiló Bosch committed Jun 17, 2018
1 parent ea982dd commit e44014e
Show file tree
Hide file tree
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
Expand Up @@ -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
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down
12 changes: 11 additions & 1 deletion src/bootstrap.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
5 changes: 3 additions & 2 deletions src/helpers/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')));
Expand All @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion src/helpers/cmsMeta/plugins/jsonapi/plugnplay.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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;
}),
])
)
)
Expand Down
11 changes: 10 additions & 1 deletion src/helpers/fetchCmsMeta.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion src/helpers/jsonrpc.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down
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
Expand Up @@ -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}`;
Expand Down
20 changes: 20 additions & 0 deletions src/routes/proxyHandler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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.