Skip to content

Commit

Permalink
Avoid importing entire crypto dependency tree if not in Node.js.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
benjamn committed Feb 12, 2019
1 parent 96351a4 commit 5f0e312
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 53 deletions.
1 change: 1 addition & 0 deletions packages/apollo-server-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
5 changes: 3 additions & 2 deletions packages/apollo-server-core/src/requestPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import {
PersistedQueryNotSupportedError,
PersistedQueryNotFoundError,
} from 'apollo-server-errors';
import { createHash } from 'crypto';
import {
GraphQLRequest,
GraphQLResponse,
Expand All @@ -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');
}
Expand Down
10 changes: 10 additions & 0 deletions packages/apollo-server-core/src/utils/createSHA.ts
Original file line number Diff line number Diff line change
@@ -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);
}
8 changes: 8 additions & 0 deletions packages/apollo-server-core/src/utils/isNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default (
typeof process === 'object' &&
process &&
process.release &&
process.release.name === 'node' &&
process.versions &&
typeof process.versions.node === 'string'
);
10 changes: 3 additions & 7 deletions packages/apollo-server-core/src/utils/runtimeSupportsUploads.ts
Original file line number Diff line number Diff line change
@@ -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));
Expand Down
88 changes: 44 additions & 44 deletions packages/apollo-server-core/src/utils/schemaHash.ts
Original file line number Diff line number Diff line change
@@ -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<typeof result>).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<typeof result>).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');
}

0 comments on commit 5f0e312

Please sign in to comment.