Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(CORS): only allow connections from the designated host (#4985)
* fix(cors): only allow localhost * fix: use host so it's configurable * fix: use cors options object * feat: use a custom graphql-server instead of the one from apollo plugin exports the httpServer instance * fix: add CORS validation in the http upgrade request Co-authored-by: Haoqun Jiang <haoqunjiang@gmail.com>
- Loading branch information
Showing
5 changed files
with
239 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
// modified from vue-cli-plugin-apollo/graphql-server | ||
// added a return value for the server() call | ||
|
||
const http = require('http') | ||
const { chalk } = require('@vue/cli-shared-utils') | ||
const express = require('express') | ||
const { ApolloServer, gql } = require('apollo-server-express') | ||
const { PubSub } = require('graphql-subscriptions') | ||
const merge = require('deepmerge') | ||
|
||
function defaultValue (provided, value) { | ||
return provided == null ? value : provided | ||
} | ||
|
||
function autoCall (fn, ...context) { | ||
if (typeof fn === 'function') { | ||
return fn(...context) | ||
} | ||
return fn | ||
} | ||
|
||
module.exports = (options, cb = null) => { | ||
// Default options | ||
options = merge({ | ||
integratedEngine: false | ||
}, options) | ||
|
||
// Express app | ||
const app = express() | ||
|
||
// Customize those files | ||
let typeDefs = load(options.paths.typeDefs) | ||
const resolvers = load(options.paths.resolvers) | ||
const context = load(options.paths.context) | ||
const schemaDirectives = load(options.paths.directives) | ||
let pubsub | ||
try { | ||
pubsub = load(options.paths.pubsub) | ||
} catch (e) { | ||
if (process.env.NODE_ENV !== 'production' && !options.quiet) { | ||
console.log(chalk.yellow('Using default PubSub implementation for subscriptions.')) | ||
console.log(chalk.grey('You should provide a different implementation in production (for example with Redis) by exporting it in \'apollo-server/pubsub.js\'.')) | ||
} | ||
} | ||
let dataSources | ||
try { | ||
dataSources = load(options.paths.dataSources) | ||
} catch (e) {} | ||
|
||
// GraphQL API Server | ||
|
||
// Realtime subscriptions | ||
if (!pubsub) pubsub = new PubSub() | ||
|
||
// Customize server | ||
try { | ||
const serverModule = load(options.paths.server) | ||
serverModule(app) | ||
} catch (e) { | ||
// No file found | ||
} | ||
|
||
// Apollo server options | ||
|
||
typeDefs = processSchema(typeDefs) | ||
|
||
let apolloServerOptions = { | ||
typeDefs, | ||
resolvers, | ||
schemaDirectives, | ||
dataSources, | ||
tracing: true, | ||
cacheControl: true, | ||
engine: !options.integratedEngine, | ||
// Resolvers context from POST | ||
context: async ({ req, connection }) => { | ||
let contextData | ||
try { | ||
if (connection) { | ||
contextData = await autoCall(context, { connection }) | ||
} else { | ||
contextData = await autoCall(context, { req }) | ||
} | ||
} catch (e) { | ||
console.error(e) | ||
throw e | ||
} | ||
contextData = Object.assign({}, contextData, { pubsub }) | ||
return contextData | ||
}, | ||
// Resolvers context from WebSocket | ||
subscriptions: { | ||
path: options.subscriptionsPath, | ||
onConnect: async (connection, websocket) => { | ||
let contextData = {} | ||
try { | ||
contextData = await autoCall(context, { | ||
connection, | ||
websocket | ||
}) | ||
contextData = Object.assign({}, contextData, { pubsub }) | ||
} catch (e) { | ||
console.error(e) | ||
throw e | ||
} | ||
return contextData | ||
} | ||
} | ||
} | ||
|
||
// Automatic mocking | ||
if (options.enableMocks) { | ||
// Customize this file | ||
apolloServerOptions.mocks = load(options.paths.mocks) | ||
apolloServerOptions.mockEntireSchema = false | ||
|
||
if (!options.quiet) { | ||
if (process.env.NODE_ENV === 'production') { | ||
console.warn('Automatic mocking is enabled, consider disabling it with the \'enableMocks\' option.') | ||
} else { | ||
console.log('✔️ Automatic mocking is enabled') | ||
} | ||
} | ||
} | ||
|
||
// Apollo Engine | ||
if (options.enableEngine && options.integratedEngine) { | ||
if (options.engineKey) { | ||
apolloServerOptions.engine = { | ||
apiKey: options.engineKey, | ||
schemaTag: options.schemaTag, | ||
...options.engineOptions || {} | ||
} | ||
console.log('✔️ Apollo Engine is enabled') | ||
} else if (!options.quiet) { | ||
console.log(chalk.yellow('Apollo Engine key not found.') + `To enable Engine, set the ${chalk.cyan('VUE_APP_APOLLO_ENGINE_KEY')} env variable.`) | ||
console.log('Create a key at https://engine.apollographql.com/') | ||
console.log('You may see `Error: Must provide document` errors (query persisting tries).') | ||
} | ||
} else { | ||
apolloServerOptions.engine = false | ||
} | ||
|
||
// Final options | ||
apolloServerOptions = merge(apolloServerOptions, defaultValue(options.serverOptions, {})) | ||
|
||
// Apollo Server | ||
const server = new ApolloServer(apolloServerOptions) | ||
|
||
// Express middleware | ||
server.applyMiddleware({ | ||
app, | ||
path: options.graphqlPath, | ||
cors: options.cors | ||
// gui: { | ||
// endpoint: graphqlPath, | ||
// subscriptionEndpoint: graphqlSubscriptionsPath, | ||
// }, | ||
}) | ||
|
||
// Start server | ||
const httpServer = http.createServer(app) | ||
httpServer.setTimeout(options.timeout) | ||
server.installSubscriptionHandlers(httpServer) | ||
|
||
httpServer.listen({ | ||
host: options.host || 'localhost', | ||
port: options.port | ||
}, () => { | ||
if (!options.quiet) { | ||
console.log(`✔️ GraphQL Server is running on ${chalk.cyan(`http://localhost:${options.port}${options.graphqlPath}`)}`) | ||
if (process.env.NODE_ENV !== 'production' && !process.env.VUE_CLI_API_MODE) { | ||
console.log(`✔️ Type ${chalk.cyan('rs')} to restart the server`) | ||
} | ||
} | ||
|
||
cb && cb() | ||
}) | ||
|
||
// added in order to let vue cli to deal with the http upgrade request | ||
return { | ||
apolloServer: server, | ||
httpServer | ||
} | ||
} | ||
|
||
function load (file) { | ||
const module = require(file) | ||
if (module.default) { | ||
return module.default | ||
} | ||
return module | ||
} | ||
|
||
function processSchema (typeDefs) { | ||
if (Array.isArray(typeDefs)) { | ||
return typeDefs.map(processSchema) | ||
} | ||
|
||
if (typeof typeDefs === 'string') { | ||
// Convert schema to AST | ||
typeDefs = gql(typeDefs) | ||
} | ||
|
||
// Remove upload scalar (it's already included in Apollo Server) | ||
removeFromSchema(typeDefs, 'ScalarTypeDefinition', 'Upload') | ||
|
||
return typeDefs | ||
} | ||
|
||
function removeFromSchema (document, kind, name) { | ||
const definitions = document.definitions | ||
const index = definitions.findIndex( | ||
def => def.kind === kind && def.name.kind === 'Name' && def.name.value === name | ||
) | ||
if (index !== -1) { | ||
definitions.splice(index, 1) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,2 @@ | ||
exports.server = require('vue-cli-plugin-apollo/graphql-server') | ||
exports.server = require('./graphql-server') | ||
exports.portfinder = require('portfinder') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters