Skip to content

Commit

Permalink
fix(CORS): only allow connections from the designated host (#4985)
Browse files Browse the repository at this point in the history
* 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
Akryum and sodatea committed Feb 3, 2020
1 parent 8028d9f commit da43343
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 34 deletions.
219 changes: 219 additions & 0 deletions 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)
}
}
2 changes: 2 additions & 0 deletions packages/@vue/cli-ui/package.json
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion 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')
13 changes: 11 additions & 2 deletions packages/@vue/cli/lib/ui.js
Expand Up @@ -36,7 +36,9 @@ async function ui (options = {}, context = process.cwd()) {
subscriptionsPath: '/graphql',
enableMocks: false,
enableEngine: false,
cors: '*',
cors: {
origin: host
},
timeout: 1000000,
quiet: true,
paths: {
Expand All @@ -49,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
Expand All @@ -66,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) => {
Expand Down
37 changes: 6 additions & 31 deletions yarn.lock
Expand Up @@ -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=
Expand Down Expand Up @@ -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==
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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==
Expand Down Expand Up @@ -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==
Expand Down Expand Up @@ -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==
Expand Down Expand Up @@ -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==
Expand Down

0 comments on commit da43343

Please sign in to comment.