1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 | 'use strict'
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 | const https = require('https')
|
27 | const { URL } = require('url')
|
28 | const path = require('path')
|
29 | const querystring = require('querystring')
|
30 |
|
31 | const handlers = require('../lib/handlers.js')
|
32 | const wrapper = require('../lib/wrapper.js')
|
33 |
|
34 |
|
35 | function 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 |
|
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 |
|
62 | async 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 = {
|
71 | 'Content-Type': 'application/json'
|
72 | }
|
73 |
|
74 |
|
75 | const config = require(path.join(__dirname, 'bm-server.json'))
|
76 |
|
77 | const finish = (
|
78 | statusCode ,
|
79 | body ,
|
80 | customHeaders
|
81 | ) => {
|
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 = {
|
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 |
|
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 |
|
181 | if (request.headers.origin) {
|
182 | if (!config.cors) {
|
183 |
|
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 |
|
192 | return finish(200)
|
193 | }
|
194 |
|
195 | internalHeaders['Access-Control-Allow-Origin'] = request.headers.origin
|
196 | internalHeaders['Access-Control-Expose-Headers'] = config.cors.exposedHeaders.join(',')
|
197 |
|
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 |
|
204 | if (config.cors.credentials) {
|
205 | internalHeaders['Access-Control-Allow-Credentials'] = true
|
206 | }
|
207 | }
|
208 | if (request.method === 'options') {
|
209 |
|
210 |
|
211 | return finish(200)
|
212 | }
|
213 |
|
214 |
|
215 |
|
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 |
|
255 | module.exports = {
|
256 | handler,
|
257 | normaliseLambdaRequest
|
258 | }
|
259 |
|