From 24f8f32028e4e8183c338a550e5c56dbf271751a Mon Sep 17 00:00:00 2001 From: Guillaume Chau Date: Fri, 20 Dec 2019 15:58:35 +0100 Subject: [PATCH 1/5] fix(cors): only allow localhost --- packages/@vue/cli/lib/ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@vue/cli/lib/ui.js b/packages/@vue/cli/lib/ui.js index 0c70f6030a..d9cc27f440 100644 --- a/packages/@vue/cli/lib/ui.js +++ b/packages/@vue/cli/lib/ui.js @@ -36,7 +36,7 @@ async function ui (options = {}, context = process.cwd()) { subscriptionsPath: '/graphql', enableMocks: false, enableEngine: false, - cors: '*', + cors: 'localhost', timeout: 1000000, quiet: true, paths: { From 25bd811b21389a19006f95d581febc099eeb5b9d Mon Sep 17 00:00:00 2001 From: Guillaume Chau Date: Fri, 20 Dec 2019 19:55:09 +0100 Subject: [PATCH 2/5] fix: use host so it's configurable --- packages/@vue/cli/lib/ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@vue/cli/lib/ui.js b/packages/@vue/cli/lib/ui.js index d9cc27f440..7c7b7c8833 100644 --- a/packages/@vue/cli/lib/ui.js +++ b/packages/@vue/cli/lib/ui.js @@ -36,7 +36,7 @@ async function ui (options = {}, context = process.cwd()) { subscriptionsPath: '/graphql', enableMocks: false, enableEngine: false, - cors: 'localhost', + cors: host, timeout: 1000000, quiet: true, paths: { From 18447c1027f6221f06c54efe2d840158cde496d3 Mon Sep 17 00:00:00 2001 From: Guillaume Chau Date: Sat, 21 Dec 2019 15:09:17 +0100 Subject: [PATCH 3/5] fix: use cors options object --- packages/@vue/cli/lib/ui.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/@vue/cli/lib/ui.js b/packages/@vue/cli/lib/ui.js index 7c7b7c8833..97f0ee5438 100644 --- a/packages/@vue/cli/lib/ui.js +++ b/packages/@vue/cli/lib/ui.js @@ -36,7 +36,9 @@ async function ui (options = {}, context = process.cwd()) { subscriptionsPath: '/graphql', enableMocks: false, enableEngine: false, - cors: host, + cors: { + origin: host + }, timeout: 1000000, quiet: true, paths: { From 30fc42dcd0e3633058c6881f3994dca953b5d9c6 Mon Sep 17 00:00:00 2001 From: Haoqun Jiang Date: Mon, 3 Feb 2020 19:02:43 +0800 Subject: [PATCH 4/5] feat: use a custom graphql-server instead of the one from apollo plugin exports the httpServer instance --- packages/@vue/cli-ui/graphql-server.js | 219 +++++++++++++++++++++++++ packages/@vue/cli-ui/package.json | 2 + packages/@vue/cli-ui/server.js | 2 +- yarn.lock | 37 +---- 4 files changed, 228 insertions(+), 32 deletions(-) create mode 100644 packages/@vue/cli-ui/graphql-server.js diff --git a/packages/@vue/cli-ui/graphql-server.js b/packages/@vue/cli-ui/graphql-server.js new file mode 100644 index 0000000000..1215994fde --- /dev/null +++ b/packages/@vue/cli-ui/graphql-server.js @@ -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) + } +} diff --git a/packages/@vue/cli-ui/package.json b/packages/@vue/cli-ui/package.json index 46d745dc5b..c7635e697a 100644 --- a/packages/@vue/cli-ui/package.json +++ b/packages/@vue/cli-ui/package.json @@ -35,8 +35,10 @@ "dependencies": { "@akryum/winattr": "^3.0.0", "@vue/cli-shared-utils": "^4.1.2", + "apollo-server-express": "^2.9.6", "clone": "^2.1.1", "deepmerge": "^4.2.2", + "express": "^4.17.1", "express-history-api-fallback": "^2.2.1", "fkill": "^6.1.0", "fs-extra": "^7.0.1", diff --git a/packages/@vue/cli-ui/server.js b/packages/@vue/cli-ui/server.js index ab60cce869..4b7bcdf82f 100644 --- a/packages/@vue/cli-ui/server.js +++ b/packages/@vue/cli-ui/server.js @@ -1,2 +1,2 @@ -exports.server = require('vue-cli-plugin-apollo/graphql-server') +exports.server = require('./graphql-server') exports.portfinder = require('portfinder') diff --git a/yarn.lock b/yarn.lock index 7dda26795b..d28ed5b3b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6734,7 +6734,7 @@ detect-indent@^6.0.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.0.0.tgz#0abd0f549f69fc6659a254fe96786186b6f528fd" integrity sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA== -detect-libc@^1.0.2, detect-libc@^1.0.3: +detect-libc@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= @@ -9608,7 +9608,7 @@ hyperlinker@^1.0.0: resolved "https://registry.yarnpkg.com/hyperlinker/-/hyperlinker-1.0.0.tgz#23dc9e38a206b208ee49bc2d6c8ef47027df0c0e" integrity sha512-Ty8UblRWFEcfSuIaajM34LdPXIhbs1ajEX/BBPv24J+enSVaEVY63xQ6lTO9VRYS5LAoghIG0IDJ+p+IPzKUQQ== -iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: +iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -12662,15 +12662,6 @@ neat-csv@^2.1.0: get-stream "^2.1.0" into-stream "^2.0.0" -needle@^2.2.1: - version "2.4.0" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c" - integrity sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg== - dependencies: - debug "^3.2.6" - iconv-lite "^0.4.4" - sax "^1.2.4" - negotiator@0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" @@ -12856,22 +12847,6 @@ node-notifier@^6.0.0: shellwords "^0.1.1" which "^1.3.1" -node-pre-gyp@*: - version "0.14.0" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz#9a0596533b877289bcad4e143982ca3d904ddc83" - integrity sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA== - dependencies: - detect-libc "^1.0.2" - mkdirp "^0.5.1" - needle "^2.2.1" - nopt "^4.0.1" - npm-packlist "^1.1.6" - npmlog "^4.0.2" - rc "^1.2.7" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^4.4.2" - node-releases@^1.1.47: version "1.1.47" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.47.tgz#c59ef739a1fd7ecbd9f0b7cf5b7871e8a8b591e4" @@ -13030,7 +13005,7 @@ npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1: semver "^5.6.0" validate-npm-package-name "^3.0.0" -npm-packlist@^1.1.6, npm-packlist@^1.4.4: +npm-packlist@^1.4.4: version "1.4.8" resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e" integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A== @@ -13069,7 +13044,7 @@ npm-run-path@^4.0.0: dependencies: path-key "^3.0.0" -npmlog@^4.0.2, npmlog@^4.1.2: +npmlog@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== @@ -14758,7 +14733,7 @@ raw-body@^2.2.0: iconv-lite "0.4.24" unpipe "1.0.0" -rc@^1.0.1, rc@^1.1.6, rc@^1.2.7, rc@^1.2.8: +rc@^1.0.1, rc@^1.1.6, rc@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== @@ -16578,7 +16553,7 @@ tar@4.4.2: safe-buffer "^5.1.2" yallist "^3.0.2" -tar@^4.4.10, tar@^4.4.12, tar@^4.4.2, tar@^4.4.8: +tar@^4.4.10, tar@^4.4.12, tar@^4.4.8: version "4.4.13" resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== From 60ff8f18ef2140eb7a78b7b400f206964c30b8b8 Mon Sep 17 00:00:00 2001 From: Haoqun Jiang Date: Mon, 3 Feb 2020 19:04:14 +0800 Subject: [PATCH 5/5] fix: add CORS validation in the http upgrade request --- packages/@vue/cli/lib/ui.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/@vue/cli/lib/ui.js b/packages/@vue/cli/lib/ui.js index 97f0ee5438..e5a2180183 100644 --- a/packages/@vue/cli/lib/ui.js +++ b/packages/@vue/cli/lib/ui.js @@ -51,7 +51,7 @@ async function ui (options = {}, context = process.cwd()) { } } - server(opts, () => { + const { httpServer } = server(opts, () => { // Reset for yarn/npm to work correctly if (typeof nodeEnv === 'undefined') { delete process.env.NODE_ENV @@ -68,6 +68,13 @@ async function ui (options = {}, context = process.cwd()) { openBrowser(url) } }) + + httpServer.on('upgrade', (req, socket) => { + const { origin } = req.headers + if (!origin || !(new RegExp(host)).test(origin)) { + socket.destroy() + } + }) } module.exports = (...args) => {