-
Notifications
You must be signed in to change notification settings - Fork 105
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(server): return HTTP 403 errors when rejecting disallowed queries
replaces apollo-server-plugin with express middleware since errors throw within are unable to be caught there too.
- Loading branch information
Showing
13 changed files
with
170 additions
and
151 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
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 @@ | ||
export type AllowList = {[key: string]: number } |
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,23 @@ | ||
import { Config } from './config' | ||
import { Server } from './Server' | ||
import { buildSchema as buildCardanoDbHasuraSchema, Db } from '@cardano-graphql/api-cardano-db-hasura' | ||
import { buildSchema as buildGenesisSchema } from '@cardano-graphql/api-genesis' | ||
import { GraphQLSchema } from 'graphql' | ||
|
||
export * from './config' | ||
|
||
export async function CompleteApiServer (config: Config): Promise<Server> { | ||
const schemas: GraphQLSchema[] = [] | ||
if (config.genesisFileByron !== undefined || config.genesisFileShelley !== undefined) { | ||
schemas.push(buildGenesisSchema({ | ||
...config.genesisFileByron !== undefined ? { byron: require(config.genesisFileByron) } : {}, | ||
...config.genesisFileShelley !== undefined ? { shelley: require(config.genesisFileShelley) } : {} | ||
})) | ||
} | ||
if (config.hasuraUri !== undefined) { | ||
const db = new Db(config.hasuraUri) | ||
await db.init() | ||
schemas.push(await buildCardanoDbHasuraSchema(config.hasuraUri, db)) | ||
} | ||
return new Server(schemas, config) | ||
} |
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,17 +1,94 @@ | ||
import { ApolloServer, ApolloServerExpressConfig } from 'apollo-server-express' | ||
import { ApolloServer, CorsOptions } from 'apollo-server-express' | ||
import express from 'express' | ||
import corsMiddleware from 'cors' | ||
import { GraphQLSchema } from 'graphql' | ||
import { mergeSchemas } from '@graphql-tools/merge' | ||
import { prometheusMetricsPlugin } from './apollo_server_plugins' | ||
import { allowListMiddleware } from './express_middleware' | ||
import depthLimit from 'graphql-depth-limit' | ||
import { PluginDefinition } from 'apollo-server-core' | ||
import { AllowList } from './AllowList' | ||
import { json } from 'body-parser' | ||
import fs from 'fs-extra' | ||
import { listenPromise } from './util' | ||
import http from 'http' | ||
|
||
export function Server ( | ||
app: express.Application, | ||
apolloServerExpressConfig: ApolloServerExpressConfig, | ||
cors?: corsMiddleware.CorsOptions | ||
): ApolloServer { | ||
const apolloServer = new ApolloServer(apolloServerExpressConfig) | ||
apolloServer.applyMiddleware({ | ||
app, | ||
cors, | ||
path: '/' | ||
}) | ||
return apolloServer | ||
export type Config = { | ||
allowIntrospection?: boolean | ||
allowListPath?: string | ||
allowedOrigins?: CorsOptions['origin'] | ||
apiPort: number | ||
cacheEnabled?: boolean | ||
prometheusMetrics?: boolean | ||
queryDepthLimit?: number | ||
tracing?: boolean | ||
} | ||
|
||
export class Server { | ||
public app: express.Application | ||
private apolloServer: ApolloServer | ||
private config: Config | ||
private httpServer: http.Server | ||
private schemas: GraphQLSchema[] | ||
|
||
constructor (schemas: GraphQLSchema[], config?: Config) { | ||
this.app = express() | ||
this.config = config | ||
this.schemas = schemas | ||
} | ||
|
||
async init () { | ||
let allowList: AllowList | ||
const plugins: PluginDefinition[] = [] | ||
const validationRules = [] | ||
if (this.config?.allowListPath) { | ||
try { | ||
const file = await fs.readFile(this.config.allowListPath, 'utf8') | ||
allowList = JSON.parse(file) | ||
this.app.use('/', json(), allowListMiddleware(allowList)) | ||
console.log('The server will only allow only operations from the provided list') | ||
} catch (error) { | ||
console.error(`Cannot read or parse allow-list JSON file at ${this.config.allowListPath}`) | ||
throw error | ||
} | ||
} | ||
if (this.config?.prometheusMetrics) { | ||
plugins.push(prometheusMetricsPlugin(this.app)) | ||
console.log('Prometheus metrics will be served at /metrics') | ||
} | ||
if (this.config?.queryDepthLimit) { | ||
validationRules.push(depthLimit(this.config?.queryDepthLimit)) | ||
} | ||
this.apolloServer = new ApolloServer({ | ||
cacheControl: this.config?.cacheEnabled ? { defaultMaxAge: 20 } : undefined, | ||
introspection: !!this.config?.allowIntrospection, | ||
playground: !!this.config?.allowIntrospection, | ||
plugins, | ||
validationRules, | ||
schema: mergeSchemas({ | ||
schemas: this.schemas | ||
}) | ||
}) | ||
this.apolloServer.applyMiddleware({ | ||
app: this.app, | ||
cors: this.config?.allowedOrigins ? { | ||
origin: this.config?.allowedOrigins | ||
} : undefined, | ||
path: '/' | ||
}) | ||
} | ||
|
||
async start () { | ||
this.httpServer = await listenPromise(this.app, { port: this.config.apiPort }) | ||
console.log( | ||
`GraphQL HTTP server at http://localhost:${this.config.apiPort}${this.apolloServer.graphqlPath}` | ||
) | ||
} | ||
|
||
shutdown () { | ||
this.httpServer.close() | ||
console.log( | ||
`GraphQL HTTP server at http://localhost:${this.config.apiPort}${this.apolloServer.graphqlPath} | ||
shutting down` | ||
) | ||
} | ||
} |
19 changes: 0 additions & 19 deletions
19
packages/server/src/apollo_server_plugins/allow_list_plugin.ts
This file was deleted.
Oops, something went wrong.
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 @@ | ||
export * from './prometheus_metrics_plugin' | ||
export * from './allow_list_plugin' |
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 |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import express from 'express' | ||
|
||
export function allowListMiddleware (allowList: {[key: string]: number }) { | ||
return (request: express.Request, response: express.Response, next: Function) => { | ||
if (allowList[request.body.query] === undefined) { | ||
response.sendStatus(403) | ||
} | ||
next() | ||
} | ||
} |
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 @@ | ||
export * from './allow_list' |
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,76 +1,15 @@ | ||
import * as apolloServerPlugins from './apollo_server_plugins' | ||
import express from 'express' | ||
import fs from 'fs' | ||
import { getConfig } from './config' | ||
import { Server } from './Server' | ||
import depthLimit from 'graphql-depth-limit' | ||
import { mergeSchemas } from '@graphql-tools/merge' | ||
import { PluginDefinition } from 'apollo-server-core' | ||
import { buildSchema as buildCardanoDbHasuraSchema, Db } from '@cardano-graphql/api-cardano-db-hasura' | ||
import { buildSchema as buildGenesisSchema } from '@cardano-graphql/api-genesis' | ||
import { GraphQLSchema } from 'graphql' | ||
export * from './config' | ||
export { apolloServerPlugins } | ||
|
||
const { prometheusMetricsPlugin, allowListPlugin } = apolloServerPlugins | ||
|
||
async function boot () { | ||
const config = await getConfig() | ||
const schemas: GraphQLSchema[] = [] | ||
const validationRules = [] | ||
const plugins: PluginDefinition[] = [] | ||
const app = express() | ||
|
||
if (config.genesisFileByron !== undefined || config.genesisFileShelley !== undefined) { | ||
schemas.push(buildGenesisSchema({ | ||
...config.genesisFileByron !== undefined ? { byron: require(config.genesisFileByron) } : {}, | ||
...config.genesisFileShelley !== undefined ? { shelley: require(config.genesisFileShelley) } : {} | ||
})) | ||
} | ||
|
||
if (config.hasuraUri !== undefined) { | ||
const db = new Db(config.hasuraUri) | ||
await db.init() | ||
schemas.push(await buildCardanoDbHasuraSchema(config.hasuraUri, db)) | ||
} | ||
import { CompleteApiServer } from './CompleteApiServer' | ||
|
||
if (config.prometheusMetrics) { | ||
plugins.push(prometheusMetricsPlugin(app)) | ||
} | ||
if (config.allowListPath) { | ||
const allowList = JSON.parse(fs.readFileSync(config.allowListPath, 'utf8')) | ||
plugins.push(allowListPlugin(allowList)) | ||
} | ||
if (config.queryDepthLimit) { | ||
validationRules.push(depthLimit(config.queryDepthLimit)) | ||
} | ||
const server = Server(app, { | ||
cacheControl: config.cacheEnabled ? { defaultMaxAge: 20 } : undefined, | ||
introspection: config.allowIntrospection, | ||
playground: config.allowIntrospection, | ||
plugins, | ||
validationRules, | ||
schema: mergeSchemas({ | ||
schemas | ||
}) | ||
}, { | ||
origin: config.allowedOrigins | ||
}) | ||
export * from './config' | ||
|
||
(async function () { | ||
try { | ||
app.listen({ port: config.apiPort }, () => { | ||
const serverUri = `http://localhost:${config.apiPort}` | ||
console.log(`GraphQL HTTP server at ${serverUri}${server.graphqlPath}`) | ||
if (process.env.NODE_ENV !== 'production' && config.allowListPath) { | ||
console.warn('As an allow-list is in effect, the GraphQL Playground is available, but will not allow schema exploration') | ||
} | ||
if (config.prometheusMetrics) { | ||
console.log(`Prometheus metrics at ${serverUri}/metrics`) | ||
} | ||
}) | ||
const server = await CompleteApiServer(await getConfig()) | ||
await server.init() | ||
await server.start() | ||
} catch (error) { | ||
console.error(error.message) | ||
process.exit(1) | ||
} | ||
} | ||
|
||
boot() | ||
})() |
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,9 @@ | ||
import { Application } from 'express' | ||
import http from 'http' | ||
|
||
export function listenPromise (app: Application, config: { port: number }): Promise<http.Server> { | ||
return new Promise(function (resolve, reject) { | ||
const server: http.Server = app.listen(config.port, () => resolve(server)) | ||
server.on('error', reject) | ||
}) | ||
} |
Oops, something went wrong.