UNPKG

5.64 kBJavaScriptView Raw
1/* @flow */
2'use strict'
3
4/* ::
5import type {
6 BmRequest,
7 CorsConfiguration
8} from '../types.js'
9type HapiCorsConfiguration = {
10 credentials: boolean,
11 exposedHeaders: Array<string>,
12 headers: Array<string>,
13 maxAge: number,
14 origin: Array<string>
15}
16
17import type {
18 BlinkMRCServer
19} from '../types.js'
20*/
21
22const Boom = require('boom')
23const Hapi = require('hapi')
24const pify = require('pify')
25
26const apis = require('./apis.js')
27const handlers = require('./handlers.js')
28const values = require('./values.js')
29const wrapper = require('./wrapper.js')
30const variables = require('./variables.js')
31const awsRoles = require('./assume-aws-roles.js')
32
33function normaliseHapiCors (
34 cors /* : CorsConfiguration | false */
35) /* : HapiCorsConfiguration | false */ {
36 if (!cors) {
37 return false
38 }
39 return {
40 credentials: cors.credentials,
41 exposedHeaders: cors.exposedHeaders,
42 headers: cors.headers,
43 maxAge: cors.maxAge,
44 origin: cors.origins
45 }
46}
47
48// return only the pertinent data from a Hapi Request
49function normaliseHapiRequest (
50 request /* : any */,
51 params /* : { [id:string]: string } */
52) /* : BmRequest */ {
53 const urlInfo = Object.assign({}, request.url, {
54 host: request.info.host,
55 hostname: request.info.hostname,
56 params,
57 protocol: wrapper.protocolFromHeaders(request.headers)
58 })
59 delete urlInfo.auth
60 delete urlInfo.hash
61 delete urlInfo.href
62 delete urlInfo.path
63 delete urlInfo.port
64 delete urlInfo.search
65 delete urlInfo.slashes
66 return {
67 body: request.payload,
68 headers: request.headers,
69 method: request.method,
70 route: '',
71 url: urlInfo
72 }
73}
74
75function startServer (
76 logger/* : typeof console */,
77 options /* : {
78 cors: CorsConfiguration | false,
79 cwd: string,
80 env: string,
81 port: string | number
82 } */,
83 config /* : BlinkMRCServer */,
84 blinkMobileIdentity /* : Object */,
85 env /* : string */
86) /* : Promise<any> */ {
87 return Promise.resolve()
88 .then(() => {
89 if (config.awsProfile) {
90 // set the AWS Profile to be picked up by the AWS JS sdk
91 process.env.AWS_PROFILE = config.awsProfile
92 } else {
93 return blinkMobileIdentity.getAccessToken()
94 .then((accessToken) => {
95 return awsRoles.assumeAWSRoleToServeLocally(config, env, accessToken)
96 })
97 .then((results) => {
98 // set the AWS environments to be picked up by the AWS JS sdk
99 process.env.AWS_ACCESS_KEY_ID = results.accessKeyId
100 process.env.AWS_SECRET_ACCESS_KEY = results.secretAccessKey
101 process.env.AWS_SESSION_TOKEN = results.sessionToken
102 })
103 }
104 })
105 .then(() => {
106 options = options || {}
107 const server = new Hapi.Server()
108 server.connection({ port: options.port })
109 server.route({
110 config: {
111 cors: normaliseHapiCors(options.cors)
112 },
113 handler (request, reply) {
114 // TODO: consider sanitising this input
115 const cwd = options.cwd
116 const route = request.params.route
117 if (!route) {
118 reply(Boom.notImplemented('Must supply a route')) // 501
119 return
120 }
121
122 apis.getRouteConfig(cwd, route)
123 .catch((err) => Promise.reject(Boom.boomify(err, {
124 statusCode: 404
125 })))
126 .then((routeConfig) => {
127 return apis.getHandlerConfig(routeConfig, request.method)
128 .then((handlerConfig) => {
129 const handler = handlerConfig.handler
130 if (typeof handler !== 'function') {
131 reply(Boom.methodNotAllowed(`${request.method.toUpperCase()} method has not been implemented`)) // 405
132 return
133 }
134 const bmRequest = normaliseHapiRequest(request, handlerConfig.params)
135 bmRequest.route = routeConfig.route
136 return handlers.executeHandler(handler, bmRequest)
137 .then((bmResponse) => {
138 const response = reply(null, bmResponse.payload)
139 .code(bmResponse.statusCode)
140 .type('application/json')
141 Object.keys(bmResponse.headers).forEach((key) => response.header(key, bmResponse.headers[key]))
142 })
143 })
144 })
145 .catch((err) => {
146 if (!err || !err.isBoom) {
147 err = Boom.boomify(err, {
148 statusCode: 500
149 })
150 }
151 // Want to show customers what went wrong locally for development.
152 // Boom hides this message for 500 status codes.
153 err.output.payload.message = err.message
154
155 logger.error(err.stack)
156 if (err.data) {
157 logger.error('Boom Data:', JSON.stringify(err.data, null, 2))
158 }
159 reply(err)
160 })
161 },
162 // HTTP HEAD is automatically provided based on GET
163 method: values.METHODS.filter((method) => method !== 'OPTIONS'),
164 path: '/{route*}' // catch-all
165 })
166 return server.register({
167 register: require('good'),
168 options: {
169 ops: false,
170 reporters: {
171 console: [{
172 module: 'good-console'
173 }, 'stdout']
174 }
175 }
176 })
177 .then(() => variables.setToCurrentProcess(logger, options.cwd, options.env))
178 .then(() => pify(server.start.bind(server))())
179 .then(() => server)
180 })
181}
182
183module.exports = {
184 normaliseHapiCors,
185 normaliseHapiRequest,
186 startServer
187}