UNPKG

8.16 kBJavaScriptView Raw
1/* eslint-disable no-console */
2
3/**
4This module exports a "handler" function,
5that wraps a customer function.
6We bundle this module and its dependencies to ../dist/wrapper.js .
7To bundle: `npm run build`
8*/
9/* @flow */
10'use strict'
11
12/* ::
13import type {
14 BmRequest,
15 Headers,
16 LambdaEvent
17} from '../types.js'
18
19type APIGatewayResult = {
20 statusCode: number,
21 headers: Headers,
22 body?: string
23}
24*/
25
26const https = require('https')
27const { URL } = require('url')
28const path = require('path')
29const querystring = require('querystring')
30
31const handlers = require('../lib/handlers.js')
32const wrapper = require('../lib/wrapper.js')
33
34// return only the pertinent data from a API Gateway + Lambda event
35function normaliseLambdaRequest (
36 event /* : LambdaEvent */
37) /* : BmRequest */ {
38 const headers = wrapper.keysToLowerCase(event.headers)
39 let body = event.body
40 try {
41 body = JSON.parse(body)
42 } catch (e) {
43 // Do nothing...
44 }
45 const host = headers['x-forwarded-host'] || headers.host
46 return {
47 body,
48 headers,
49 method: wrapper.normaliseMethod(event.httpMethod),
50 route: event.path,
51 url: {
52 host,
53 hostname: host,
54 params: {},
55 pathname: event.path,
56 protocol: wrapper.protocolFromHeaders(headers),
57 query: event.queryStringParameters || {}
58 }
59 }
60}
61
62async function handler (
63 event /* : LambdaEvent */,
64 context /* : any */
65) /* : Promise<APIGatewayResult> */ {
66 const startTime = Date.now()
67 context.callbackWaitsForEmptyEventLoop = false
68
69 const request = normaliseLambdaRequest(event)
70 const internalHeaders /* : Headers */ = {
71 'Content-Type': 'application/json'
72 }
73
74 // $FlowFixMe requiring file without string literal to accommodate for __dirname
75 const config = require(path.join(__dirname, 'bm-server.json'))
76
77 const finish = (
78 statusCode /* : number */,
79 body /* : mixed | void */,
80 customHeaders /* : Headers | void */
81 ) /* : APIGatewayResult */ => {
82 const headers = wrapper.keysToLowerCase(Object.assign(internalHeaders, customHeaders))
83 const endTime = Date.now()
84 const requestTime = endTime - startTime
85
86 if (
87 process.env.ONEBLINK_ANALYTICS_ORIGIN &&
88 process.env.ONEBLINK_ANALYTICS_COLLECTOR_TOKEN
89 ) {
90 try {
91 const token = process.env.ONEBLINK_ANALYTICS_COLLECTOR_TOKEN
92 const hostname = new URL(process.env.ONEBLINK_ANALYTICS_ORIGIN).hostname
93 const httpsRequest = https.request({
94 hostname,
95 path: '/events',
96 method: 'POST',
97 headers: {
98 'Content-Type': 'application/json',
99 Authorization: `Bearer ${token}`
100 }
101 })
102 httpsRequest.write(JSON.stringify({
103 events: [
104 {
105 name: 'Server CLI Request',
106 date: new Date().toISOString(),
107 tags: {
108 env: config.env,
109 scope: config.scope,
110 request: {
111 method: request.method.toUpperCase(),
112 query: request.url.query,
113 port: 443,
114 path: request.route,
115 hostName: request.url.hostname,
116 params: request.url.params,
117 protocol: request.url.protocol
118 },
119 response: {
120 statusCode: statusCode
121 },
122 requestTime: {
123 startDateTime: new Date(startTime).toISOString(),
124 startTimeStamp: startTime,
125 endDateTime: new Date(endTime).toISOString(),
126 endTimeStamp: endTime,
127 ms: requestTime,
128 s: requestTime / 1000
129 }
130 }
131 }
132 ]
133 }))
134 httpsRequest.end()
135 } catch (e) {
136 console.warn('An error occurred attempting to POST analytics event', e)
137 }
138 }
139
140 let path = request.url.pathname
141 const search = querystring.stringify(request.url.query)
142 if (search) {
143 path += `?${search}`
144 }
145 let referrer = request.headers.referrer
146 if (typeof referrer !== 'string' || !referrer) {
147 referrer = '-'
148 }
149 let userAgent = request.headers['user-agent']
150 if (typeof userAgent !== 'string' || !userAgent) {
151 userAgent = '-'
152 }
153 console.log(`${request.method.toUpperCase()} ${path}${querystring.stringify(request.url.query)} ${statusCode} "${requestTime} ms" "${referrer}" "${userAgent}"`)
154
155 const result /* : APIGatewayResult */ = {
156 headers: headers,
157 statusCode: statusCode
158 }
159 if (body !== undefined) {
160 result.body = typeof body === 'string' ? body : JSON.stringify(body)
161 }
162 return result
163 }
164
165 try {
166 // Get handler module based on route
167 let routeConfig
168 try {
169 routeConfig = handlers.findRouteConfig(event.path, config.routes)
170 request.url.params = routeConfig.params || {}
171 request.route = routeConfig.route
172 } catch (error) {
173 return finish(404, {
174 error: 'Not Found',
175 message: error.message,
176 statusCode: 404
177 })
178 }
179
180 // Check for browser requests and apply CORS if required
181 if (request.headers.origin) {
182 if (!config.cors) {
183 // No cors, we will return 405 result and let browser handler error
184 return finish(405, {
185 error: 'Method Not Allowed',
186 message: 'OPTIONS method has not been implemented',
187 statusCode: 405
188 })
189 }
190 if (!config.cors.origins.some((origin) => origin === '*' || origin === request.headers.origin)) {
191 // Invalid origin, we will return 200 result and let browser handler error
192 return finish(200)
193 }
194 // Headers for all cross origin requests
195 internalHeaders['Access-Control-Allow-Origin'] = request.headers.origin
196 internalHeaders['Access-Control-Expose-Headers'] = config.cors.exposedHeaders.join(',')
197 // Headers for OPTIONS cross origin requests
198 if (request.method === 'options' && request.headers['access-control-request-method']) {
199 internalHeaders['Access-Control-Allow-Headers'] = config.cors.headers.join(',')
200 internalHeaders['Access-Control-Allow-Methods'] = request.headers['access-control-request-method']
201 internalHeaders['Access-Control-Max-Age'] = config.cors.maxAge
202 }
203 // Only set credentials header if truthy
204 if (config.cors.credentials) {
205 internalHeaders['Access-Control-Allow-Credentials'] = true
206 }
207 }
208 if (request.method === 'options') {
209 // For OPTIONS requests, we can just finish
210 // as we have created our own implementation of CORS
211 return finish(200)
212 }
213
214 // Change current working directory to the project
215 // to accommodate for packages using process.cwd()
216 const projectPath = path.join(__dirname, 'project')
217 if (process.cwd() !== projectPath) {
218 try {
219 process.chdir(projectPath)
220 } catch (err) {
221 throw new Error(`Could not change current working directory to '${projectPath}': ${err}`)
222 }
223 }
224
225 const handler = await handlers.getHandler(path.join(__dirname, routeConfig.module), request.method)
226 if (typeof handler !== 'function') {
227 return finish(405, {
228 error: 'Method Not Allowed',
229 message: `${request.method.toUpperCase()} method has not been implemented`,
230 statusCode: 405
231 })
232 }
233
234 const response = await handlers.executeHandler(handler, request)
235 return finish(response.statusCode, response.payload, response.headers)
236 } catch (error) {
237 if (error && error.isBoom && error.output && error.output.payload && error.output.statusCode) {
238 if (error.data) {
239 console.error(error, JSON.stringify(error.data))
240 } else {
241 console.error(error)
242 }
243 return finish(error.output.statusCode, error.output.payload, error.output.headers)
244 }
245
246 console.error(error)
247 return finish(500, {
248 error: 'Internal Server Error',
249 message: 'An internal server error occurred',
250 statusCode: 500
251 })
252 }
253}
254
255module.exports = {
256 handler,
257 normaliseLambdaRequest
258}
259/* eslint-enable no-console */