From 3f7a7f3d67a9587517efb539966d7f1a28643831 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Tue, 12 Feb 2019 14:07:42 -0500 Subject: [PATCH] Avoid importing entire crypto dependency tree if not in Node.js. (#2304) The apollo-server-core package uses Node's built-in crypto module only to create SHA-256 and -512 hashes. When we're actually running in Node, the native crypto library is clearly the best way to create these hashes, not least because we can assume it will be available without having to bundle it first. Outside of Node (such as in React Native apps), bundlers tend to fall back on the crypto-browserify polyfill, which comprises more than a hundred separate modules. Importing this polyfill at runtime (likely during application startup) takes precious time and memory, even though almost all of it is unused. Since we only need to create SHA hashes, we can import the much smaller sha.js library in non-Node environments, which happens to be what crypto-browserify uses for SHA hashing, and is a widely used npm package in its own right: https://www.npmjs.com/package/sha.js. --- packages/apollo-server-core/package.json | 1 + .../apollo-server-core/src/requestPipeline.ts | 5 +- .../apollo-server-core/src/utils/createSHA.ts | 10 +++ .../apollo-server-core/src/utils/isNode.ts | 6 ++ .../src/utils/runtimeSupportsUploads.ts | 10 +-- .../src/utils/schemaHash.ts | 88 +++++++++---------- 6 files changed, 67 insertions(+), 53 deletions(-) create mode 100644 packages/apollo-server-core/src/utils/createSHA.ts create mode 100644 packages/apollo-server-core/src/utils/isNode.ts diff --git a/packages/apollo-server-core/package.json b/packages/apollo-server-core/package.json index a42636bc631..d29c5e0b5ac 100644 --- a/packages/apollo-server-core/package.json +++ b/packages/apollo-server-core/package.json @@ -41,6 +41,7 @@ "graphql-tag": "^2.9.2", "graphql-tools": "^4.0.0", "graphql-upload": "^8.0.2", + "sha.js": "^2.4.11", "subscriptions-transport-ws": "^0.9.11", "ws": "^6.0.0" }, diff --git a/packages/apollo-server-core/src/requestPipeline.ts b/packages/apollo-server-core/src/requestPipeline.ts index 23d57646f69..e7d39f09126 100644 --- a/packages/apollo-server-core/src/requestPipeline.ts +++ b/packages/apollo-server-core/src/requestPipeline.ts @@ -29,7 +29,6 @@ import { PersistedQueryNotSupportedError, PersistedQueryNotFoundError, } from 'apollo-server-errors'; -import { createHash } from 'crypto'; import { GraphQLRequest, GraphQLResponse, @@ -53,8 +52,10 @@ export { InvalidGraphQLRequestError, }; +import createSHA from './utils/createSHA'; + function computeQueryHash(query: string) { - return createHash('sha256') + return createSHA('sha256') .update(query) .digest('hex'); } diff --git a/packages/apollo-server-core/src/utils/createSHA.ts b/packages/apollo-server-core/src/utils/createSHA.ts new file mode 100644 index 00000000000..31102aec3bd --- /dev/null +++ b/packages/apollo-server-core/src/utils/createSHA.ts @@ -0,0 +1,10 @@ +import isNode from './isNode'; + +export default function(kind: string): import('crypto').Hash { + if (isNode) { + // Use module.require instead of just require to avoid bundling whatever + // crypto polyfills a non-Node bundler might fall back to. + return module.require('crypto').createHash(kind); + } + return require('sha.js')(kind); +} diff --git a/packages/apollo-server-core/src/utils/isNode.ts b/packages/apollo-server-core/src/utils/isNode.ts new file mode 100644 index 00000000000..8002526c0ac --- /dev/null +++ b/packages/apollo-server-core/src/utils/isNode.ts @@ -0,0 +1,6 @@ +export default typeof process === 'object' && + process && + process.release && + process.release.name === 'node' && + process.versions && + typeof process.versions.node === 'string'; diff --git a/packages/apollo-server-core/src/utils/runtimeSupportsUploads.ts b/packages/apollo-server-core/src/utils/runtimeSupportsUploads.ts index f1a50344a6d..ef80c3d948e 100644 --- a/packages/apollo-server-core/src/utils/runtimeSupportsUploads.ts +++ b/packages/apollo-server-core/src/utils/runtimeSupportsUploads.ts @@ -1,11 +1,7 @@ +import isNode from './isNode'; + const runtimeSupportsUploads = (() => { - if ( - process && - process.release && - process.release.name === 'node' && - process.versions && - typeof process.versions.node === 'string' - ) { + if (isNode) { const [nodeMajor, nodeMinor] = process.versions.node .split('.', 2) .map(segment => parseInt(segment, 10)); diff --git a/packages/apollo-server-core/src/utils/schemaHash.ts b/packages/apollo-server-core/src/utils/schemaHash.ts index a1b01b3628f..7c30c6e751d 100644 --- a/packages/apollo-server-core/src/utils/schemaHash.ts +++ b/packages/apollo-server-core/src/utils/schemaHash.ts @@ -1,44 +1,44 @@ -import { parse } from 'graphql/language'; -import { execute, ExecutionResult } from 'graphql/execution'; -import { getIntrospectionQuery, IntrospectionSchema } from 'graphql/utilities'; -import stableStringify from 'fast-json-stable-stringify'; -import { GraphQLSchema } from 'graphql/type'; -import { createHash } from 'crypto'; - -export function generateSchemaHash(schema: GraphQLSchema): string { - const introspectionQuery = getIntrospectionQuery(); - const documentAST = parse(introspectionQuery); - const result = execute(schema, documentAST) as ExecutionResult; - - // If the execution of an introspection query results in a then-able, it - // indicates that one or more of its resolvers is behaving in an asynchronous - // manner. This is not the expected behavior of a introspection query - // which does not have any asynchronous resolvers. - if ( - result && - typeof (result as PromiseLike).then === 'function' - ) { - throw new Error( - [ - 'The introspection query is resolving asynchronously; execution of an introspection query is not expected to return a `Promise`.', - '', - 'Wrapped type resolvers should maintain the existing execution dynamics of the resolvers they wrap (i.e. async vs sync) or introspection types should be excluded from wrapping by checking them with `graphql/type`s, `isIntrospectionType` predicate function prior to wrapping.', - ].join('\n'), - ); - } - - if (!result || !result.data || !result.data.__schema) { - throw new Error('Unable to generate server introspection document.'); - } - - const introspectionSchema: IntrospectionSchema = result.data.__schema; - - // It's important that we perform a deterministic stringification here - // since, depending on changes in the underlying `graphql-js` execution - // layer, varying orders of the properties in the introspection - const stringifiedSchema = stableStringify(introspectionSchema); - - return createHash('sha512') - .update(stringifiedSchema) - .digest('hex'); -} +import { parse } from 'graphql/language'; +import { execute, ExecutionResult } from 'graphql/execution'; +import { getIntrospectionQuery, IntrospectionSchema } from 'graphql/utilities'; +import stableStringify from 'fast-json-stable-stringify'; +import { GraphQLSchema } from 'graphql/type'; +import createSHA from './createSHA'; + +export function generateSchemaHash(schema: GraphQLSchema): string { + const introspectionQuery = getIntrospectionQuery(); + const documentAST = parse(introspectionQuery); + const result = execute(schema, documentAST) as ExecutionResult; + + // If the execution of an introspection query results in a then-able, it + // indicates that one or more of its resolvers is behaving in an asynchronous + // manner. This is not the expected behavior of a introspection query + // which does not have any asynchronous resolvers. + if ( + result && + typeof (result as PromiseLike).then === 'function' + ) { + throw new Error( + [ + 'The introspection query is resolving asynchronously; execution of an introspection query is not expected to return a `Promise`.', + '', + 'Wrapped type resolvers should maintain the existing execution dynamics of the resolvers they wrap (i.e. async vs sync) or introspection types should be excluded from wrapping by checking them with `graphql/type`s, `isIntrospectionType` predicate function prior to wrapping.', + ].join('\n'), + ); + } + + if (!result || !result.data || !result.data.__schema) { + throw new Error('Unable to generate server introspection document.'); + } + + const introspectionSchema: IntrospectionSchema = result.data.__schema; + + // It's important that we perform a deterministic stringification here + // since, depending on changes in the underlying `graphql-js` execution + // layer, varying orders of the properties in the introspection + const stringifiedSchema = stableStringify(introspectionSchema); + + return createSHA('sha512') + .update(stringifiedSchema) + .digest('hex'); +}