From ccdd0c0a8bd921e44e8336ef36bb30646ffd1eba Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Wed, 1 Nov 2017 12:24:00 +0100 Subject: [PATCH] Add hash template tag (#560) --- .../__tests__/file-extension.test.js | 4 +- .../__tests__/hash-extension.test.js | 73 +++++++++++++++++++ app/templating/extensions/hash-extension.js | 49 +++++++++++++ app/templating/extensions/index.js | 6 +- app/templating/extensions/uuid-extension.js | 6 +- app/ui/components/templating/tag-editor.js | 5 +- flow-typed/uuid.js | 3 +- 7 files changed, 137 insertions(+), 9 deletions(-) create mode 100644 app/templating/extensions/__tests__/hash-extension.test.js create mode 100644 app/templating/extensions/hash-extension.js diff --git a/app/templating/extensions/__tests__/file-extension.test.js b/app/templating/extensions/__tests__/file-extension.test.js index b3a7cc0757..63557790af 100644 --- a/app/templating/extensions/__tests__/file-extension.test.js +++ b/app/templating/extensions/__tests__/file-extension.test.js @@ -5,7 +5,7 @@ import {globalBeforeEach} from '../../../__jest__/before-each'; function assertTemplate (txt, context, expected) { return async function () { const result = await templating.render(txt, {context}); - expect(result).toMatch(expected); + expect(result).toBe(expected); }; } @@ -24,7 +24,7 @@ describe('FileExtension', () => { beforeEach(globalBeforeEach); const ctx = {path: path.resolve(__dirname, path.join('./test.txt'))}; const escapedPath = ctx.path.replace(/\\/g, '\\\\'); - it('reads from string', assertTemplate(`{% file "${escapedPath}" %}`, ctx, 'Hello World')); + it('reads from string', assertTemplate(`{% file "${escapedPath}" %}`, ctx, 'Hello World!')); it('reads a file correctly', assertTemplate('{% file path %}', ctx, 'Hello World!')); it('fails on missing file', assertTemplateFails('{% file "/foo" %}', ctx, `ENOENT: no such file or directory, open '${path.resolve('/foo')}'`)); it('fails on no 2nd param', assertTemplateFails('{% file %}', ctx, 'No file selected')); diff --git a/app/templating/extensions/__tests__/hash-extension.test.js b/app/templating/extensions/__tests__/hash-extension.test.js new file mode 100644 index 0000000000..8a0c05a449 --- /dev/null +++ b/app/templating/extensions/__tests__/hash-extension.test.js @@ -0,0 +1,73 @@ +import * as templating from '../../index'; +import {globalBeforeEach} from '../../../__jest__/before-each'; + +function assertTemplate (txt, expected) { + return async function () { + const result = await templating.render(txt); + expect(result).toBe(expected); + }; +} + +function assertTemplateFails (txt, expected) { + return async function () { + try { + await templating.render(txt); + fail(`Render should have thrown ${expected}`); + } catch (err) { + expect(err.message).toContain(expected); + } + }; +} + +describe('FileExtension', () => { + beforeEach(globalBeforeEach); + // Algorithms + it('hashes sha1', assertTemplate( + '{% hash "sha1", "hex", "foo" %}', + '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' + )); + it('hashes sha256', assertTemplate( + '{% hash "sha256", "hex", "foo" %}', + '2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae' + )); + it('hashes md5', assertTemplate( + '{% hash "md5", "hex", "foo" %}', + 'acbd18db4cc2f85cedef654fccc4a4d8' + )); + it('fails to hash invalid algorithm', assertTemplateFails( + '{% hash "bad", "hex", "foo" %}', + 'Digest method not supported' + )); + + // Digests + it('hashes to latin1', assertTemplate( + '{% hash "md5", "latin1", "foo" %}', + '¬½ÛLÂø\\íïeOÌĤØ' + )); + it('hashes to hex', assertTemplate( + '{% hash "md5", "hex", "foo" %}', + 'acbd18db4cc2f85cedef654fccc4a4d8' + )); + it('hashes to base64', assertTemplate( + '{% hash "md5", "base64", "foo" %}', + 'rL0Y20zC+Fzt72VPzMSk2A==' + )); + it('fails to hash to invalid', assertTemplateFails( + '{% hash "md5", "bad", "foo" %}', + 'Invalid encoding bad. Choices are hex, latin1, base64' + )); + + // Values + it('hashes empty string', assertTemplate( + '{% hash "md5", "hex", "" %}', + 'd41d8cd98f00b204e9800998ecf8427e' + )); + it('hashes no string', assertTemplate( + '{% hash "md5", "hex" %}', + 'd41d8cd98f00b204e9800998ecf8427e' + )); + it('fails to hash non string', assertTemplateFails( + '{% hash "md5", "hex", true %}', + 'Cannot hash value of type "boolean"' + )); +}); diff --git a/app/templating/extensions/hash-extension.js b/app/templating/extensions/hash-extension.js new file mode 100644 index 0000000000..1b9174eac0 --- /dev/null +++ b/app/templating/extensions/hash-extension.js @@ -0,0 +1,49 @@ +// @flow +import crypto from 'crypto'; +import type {PluginTemplateTag} from './index'; + +export default ({ + name: 'hash', + displayName: 'Hash', + description: 'Apply hash to a value', + args: [ + { + displayName: 'Algorithm', + type: 'enum', + options: [ + {displayName: 'MD5', value: 'md5'}, + {displayName: 'SHA1', value: 'sha1'}, + {displayName: 'SHA256', value: 'sha256'}, + {displayName: 'SHA512', value: 'sha512'} + ] + }, + { + displayName: 'Digest Encoding', + description: 'The encoding of the output', + type: 'enum', + options: [ + {displayName: 'Hexadecimal', value: 'hex'}, + {displayName: 'Base64', value: 'base64'} + ] + }, + { + displayName: 'Input', + type: 'string', + placeholder: 'Value to hash' + } + ], + run (context: Object, algorithm: string, encoding: string, value: string = ''): string { + if (encoding !== 'hex' && encoding !== 'latin1' && encoding !== 'base64') { + throw new Error(`Invalid encoding ${encoding}. Choices are hex, latin1, base64`); + } + + const valueType = typeof value; + if (valueType !== 'string') { + throw new Error(`Cannot hash value of type "${valueType}"`); + } + + const hash = crypto.createHash(algorithm); + hash.update(value || '', 'utf8'); + return hash.digest(encoding); + } +}: PluginTemplateTag); diff --git a/app/templating/extensions/index.js b/app/templating/extensions/index.js index 654937671b..b199ef1231 100644 --- a/app/templating/extensions/index.js +++ b/app/templating/extensions/index.js @@ -7,6 +7,7 @@ import nowExtension from './now-extension'; import fileExtension from './file-extension'; import responseExtension from './response-extension'; import base64Extension from './base-64-extension'; +import hashExtension from './hash-extension'; import requestExtension from './request-extension'; import type {NunjucksParsedTagArg} from '../utils'; import type {Request} from '../../models/request'; @@ -24,8 +25,8 @@ type PluginArgumentBase = { export type PluginArgumentEnumOption = { displayName: DisplayName, - description: string, value: PluginArgumentValue, + description?: string, placeholder?: string } @@ -96,10 +97,11 @@ export type PluginTemplateTag = { const DEFAULT_EXTENSIONS: Array = [ timestampExtension, + fileExtension, nowExtension, uuidExtension, base64Extension, - fileExtension, + hashExtension, requestExtension, responseExtension ]; diff --git a/app/templating/extensions/uuid-extension.js b/app/templating/extensions/uuid-extension.js index 2cf6e9e471..61cbb000c2 100644 --- a/app/templating/extensions/uuid-extension.js +++ b/app/templating/extensions/uuid-extension.js @@ -1,6 +1,8 @@ +// @flow import uuid from 'uuid'; +import type {PluginTemplateTag} from './index'; -export default { +export default ({ displayName: 'UUID', name: 'uuid', description: 'generate v1 or v4 UUIDs', @@ -30,4 +32,4 @@ export default { throw new Error(`Invalid UUID type "${uuidType}"`); } } -}; +}: PluginTemplateTag); diff --git a/app/ui/components/templating/tag-editor.js b/app/ui/components/templating/tag-editor.js index 89eba84db7..cc4c91419c 100644 --- a/app/ui/components/templating/tag-editor.js +++ b/app/ui/components/templating/tag-editor.js @@ -384,8 +384,9 @@ class TagEditor extends React.PureComponent {