import corsMiddleware from 'cors'; import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.js'; import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@apollo/server/express4'; import { ApolloServerPluginCacheControlDisabled } from '@apollo/server/plugin/disabled'; import express from 'express'; import { execute, subscribe, GraphQLError } from 'graphql'; import { SubscriptionServer } from 'subscriptions-transport-ws'; import { handleParseErrors, handleParseHeaders, handleParseSession } from '../middlewares'; import requiredParameter from '../requiredParameter'; import defaultLogger from '../logger'; import { ParseGraphQLSchema } from './ParseGraphQLSchema'; import ParseGraphQLController, { ParseGraphQLConfig } from '../Controllers/ParseGraphQLController'; const IntrospectionControlPlugin = (publicIntrospection) => ({ requestDidStart: (requestContext) => ({ didResolveOperation: async () => { // If public introspection is enabled, we allow all introspection queries if (publicIntrospection) { return; } const isMasterOrMaintenance = requestContext.contextValue.auth?.isMaster || requestContext.contextValue.auth?.isMaintenance if (isMasterOrMaintenance) { return; } // Now we check if the query is an introspection query // this check strategy should work in 99.99% cases // we can have an issue if a user name a field or class __schemaSomething // we want to avoid a full AST check const isIntrospectionQuery = requestContext.request.query?.includes('__schema') if (isIntrospectionQuery) { throw new GraphQLError('Introspection is not allowed', { extensions: { http: { status: 403, }, } }); } }, }) }); class ParseGraphQLServer { parseGraphQLController: ParseGraphQLController; constructor(parseServer, config) { this.parseServer = parseServer || requiredParameter('You must provide a parseServer instance!'); if (!config || !config.graphQLPath) { requiredParameter('You must provide a config.graphQLPath!'); } this.config = config; this.parseGraphQLController = this.parseServer.config.parseGraphQLController; this.log = (this.parseServer.config && this.parseServer.config.loggerController) || defaultLogger; this.parseGraphQLSchema = new ParseGraphQLSchema({ parseGraphQLController: this.parseGraphQLController, databaseController: this.parseServer.config.databaseController, log: this.log, graphQLCustomTypeDefs: this.config.graphQLCustomTypeDefs, appId: this.parseServer.config.appId, }); } async _getGraphQLOptions() { try { return { schema: await this.parseGraphQLSchema.load(), context: async ({ req, res }) => { res.set('access-control-allow-origin', req.get('origin') || '*'); return { info: req.info, config: req.config, auth: req.auth, }; }, }; } catch (e) { this.log.error(e.stack || (typeof e.toString === 'function' && e.toString()) || e); throw e; } } async _getServer() { const schemaRef = this.parseGraphQLSchema.graphQLSchema; const newSchemaRef = await this.parseGraphQLSchema.load(); if (schemaRef === newSchemaRef && this._server) { return this._server; } const { schema, context } = await this._getGraphQLOptions(); const apollo = new ApolloServer({ csrfPrevention: { // See https://www.apollographql.com/docs/router/configuration/csrf/ // needed since we use graphql upload requestHeaders: ['X-Parse-Application-Id'], }, introspection: this.config.graphQLPublicIntrospection, plugins: [ApolloServerPluginCacheControlDisabled(), IntrospectionControlPlugin(this.config.graphQLPublicIntrospection)], schema, }); await apollo.start(); this._server = expressMiddleware(apollo, { context, }); return this._server; } _transformMaxUploadSizeToBytes(maxUploadSize) { const unitMap = { kb: 1, mb: 2, gb: 3, }; return ( Number(maxUploadSize.slice(0, -2)) * Math.pow(1024, unitMap[maxUploadSize.slice(-2).toLowerCase()]) ); } /** * @static * Allow developers to customize each request with inversion of control/dependency injection */ applyRequestContextMiddleware(api, options) { if (options.requestContextMiddleware) { if (typeof options.requestContextMiddleware !== 'function') { throw new Error('requestContextMiddleware must be a function'); } api.use(this.config.graphQLPath, options.requestContextMiddleware); } } applyGraphQL(app) { if (!app || !app.use) { requiredParameter('You must provide an Express.js app instance!'); } app.use(this.config.graphQLPath, corsMiddleware()); app.use(this.config.graphQLPath, handleParseHeaders); app.use(this.config.graphQLPath, handleParseSession); this.applyRequestContextMiddleware(app, this.parseServer.config); app.use(this.config.graphQLPath, handleParseErrors); app.use( this.config.graphQLPath, graphqlUploadExpress({ maxFileSize: this._transformMaxUploadSizeToBytes( this.parseServer.config.maxUploadSize || '20mb' ), }) ); app.use(this.config.graphQLPath, express.json(), async (req, res, next) => { const server = await this._getServer(); return server(req, res, next); }); } applyPlayground(app) { if (!app || !app.get) { requiredParameter('You must provide an Express.js app instance!'); } app.get( this.config.playgroundPath || requiredParameter('You must provide a config.playgroundPath to applyPlayground!'), (_req, res) => { res.setHeader('Content-Type', 'text/html'); res.write( `
` ); res.end(); } ); } createSubscriptions(server) { SubscriptionServer.create( { execute, subscribe, onOperation: async (_message, params, webSocket) => Object.assign({}, params, await this._getGraphQLOptions(webSocket.upgradeReq)), }, { server, path: this.config.subscriptionsPath || requiredParameter('You must provide a config.subscriptionsPath to createSubscriptions!'), } ); } setGraphQLConfig(graphQLConfig: ParseGraphQLConfig): Promise { return this.parseGraphQLController.updateGraphQLConfig(graphQLConfig); } } export { ParseGraphQLServer };