1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.wrapper = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
|
2 |
|
3 | 'use strict'
|
4 |
|
5 | const createInternal = require('./utils/internal.js').createInternal
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 | const internal = createInternal()
|
12 |
|
13 | class BmResponse {
|
14 | constructor () {
|
15 | Object.assign(internal(this), {
|
16 | headers: {},
|
17 | payload: undefined,
|
18 | statusCode: 200
|
19 | })
|
20 | }
|
21 |
|
22 | get headers () /* : Headers */ {
|
23 | return Object.assign({}, internal(this).headers)
|
24 | }
|
25 |
|
26 | get payload () /* : any */ {
|
27 | return internal(this).payload
|
28 | }
|
29 |
|
30 | get statusCode () /* : number */ {
|
31 | return internal(this).statusCode
|
32 | }
|
33 |
|
34 | setHeader (
|
35 | key ,
|
36 | value
|
37 | ) {
|
38 | key = key.toLowerCase()
|
39 | internal(this).headers[key] = value
|
40 | return this
|
41 | }
|
42 |
|
43 | setPayload (
|
44 | payload
|
45 | ) {
|
46 | internal(this).payload = payload
|
47 | return this
|
48 | }
|
49 |
|
50 | setStatusCode (
|
51 | code
|
52 | ) {
|
53 | internal(this).statusCode = code
|
54 | return this
|
55 | }
|
56 | }
|
57 |
|
58 | module.exports = BmResponse
|
59 |
|
60 | },{"./utils/internal.js":3}],2:[function(require,module,exports){
|
61 |
|
62 | 'use strict'
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 | const uniloc = require('uniloc')
|
73 |
|
74 | const BmResponse = require('../lib/bm-response.js')
|
75 |
|
76 | function executeHandler (
|
77 | handler /* : Handler */,
|
78 | request /* : BmRequest */
|
79 | ) /* : Promise<BmResponse> */ {
|
80 | const response = new BmResponse()
|
81 | return Promise.resolve()
|
82 | .then(() => handler(request, response))
|
83 | .then((result) => {
|
84 |
|
85 |
|
86 |
|
87 | if (result && result !== response) {
|
88 | if (Number.isFinite(result)) {
|
89 | response.setStatusCode(result)
|
90 | } else {
|
91 | response.setPayload(result)
|
92 | }
|
93 | }
|
94 | return response
|
95 | })
|
96 | }
|
97 |
|
98 | function getHandler (
|
99 | module /* : string */,
|
100 | method /* : string */
|
101 | ) /* : Promise<Handler | void> */ {
|
102 | try {
|
103 |
|
104 | let handler = require(module)
|
105 | if (handler && method && typeof handler[method] === 'function') {
|
106 | handler = handler[method]
|
107 | }
|
108 | return Promise.resolve(handler)
|
109 | } catch (err) {
|
110 | return Promise.reject(err)
|
111 | }
|
112 | }
|
113 |
|
114 | function findRouteConfig (
|
115 | route /* : string */,
|
116 | routeConfigs /* : RouteConfiguration[] */
|
117 | ) /* : RouteConfiguration */ {
|
118 | const unilocRoutes = routeConfigs.reduce((memo, r) => {
|
119 | memo[r.route] = `GET ${r.route.replace(/{/g, ':').replace(/}/g, '')}`
|
120 | return memo
|
121 | }, {})
|
122 | const unilocRouter = uniloc(unilocRoutes)
|
123 | const unilocRoute = unilocRouter.lookup(route, 'GET')
|
124 |
|
125 | const routeConfig = routeConfigs.find((routeConfig) => routeConfig.route === unilocRoute.name)
|
126 | if (!routeConfig) {
|
127 | throw new Error(`Route has not been implemented: ${route}`)
|
128 | }
|
129 |
|
130 | routeConfig.params = unilocRoute.options
|
131 | return routeConfig
|
132 | }
|
133 |
|
134 | module.exports = {
|
135 | findRouteConfig,
|
136 | executeHandler,
|
137 | getHandler
|
138 | }
|
139 |
|
140 | },{"../lib/bm-response.js":1,"uniloc":5}],3:[function(require,module,exports){
|
141 |
|
142 |
|
143 |
|
144 |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 | function createInternal () {
|
150 | const map = new WeakMap()
|
151 | return (object ) => {
|
152 | const values = map.get(object) || {}
|
153 | if (!map.has(object)) {
|
154 | map.set(object, values)
|
155 | }
|
156 | return values
|
157 | }
|
158 | }
|
159 |
|
160 | module.exports = {
|
161 | createInternal
|
162 | }
|
163 |
|
164 | },{}],4:[function(require,module,exports){
|
165 |
|
166 | 'use strict'
|
167 |
|
168 |
|
169 |
|
170 |
|
171 |
|
172 |
|
173 |
|
174 |
|
175 |
|
176 |
|
177 | function keysToLowerCase (
|
178 | object /* : MapObject */
|
179 | ) /* : MapObject */ {
|
180 | return Object.keys(object).reduce((result, key) => {
|
181 | result[key.toLowerCase()] = object[key]
|
182 | return result
|
183 | }, {})
|
184 | }
|
185 |
|
186 | function normaliseMethod (
|
187 | method /* : string */
|
188 | ) /* : string */ {
|
189 | return method.toLowerCase()
|
190 | }
|
191 |
|
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
197 | function protocolFromHeaders (
|
198 | headers /* : Headers */
|
199 | ) /* : Protocol */ {
|
200 | if (headers['x-forwarded-proto'] === 'https') {
|
201 | return 'https:'
|
202 | }
|
203 | if (typeof headers.forwarded === 'string' && ~headers.forwarded.indexOf('proto=https')) {
|
204 | return 'https:'
|
205 | }
|
206 | if (headers['front-end-https'] === 'on') {
|
207 | return 'https:'
|
208 | }
|
209 | return 'http:'
|
210 | }
|
211 |
|
212 | module.exports = {
|
213 | keysToLowerCase,
|
214 | normaliseMethod,
|
215 | protocolFromHeaders
|
216 | }
|
217 |
|
218 | },{}],5:[function(require,module,exports){
|
219 | (function(root) {
|
220 | function assert(condition, format) {
|
221 | if (!condition) {
|
222 | var args = [].slice.call(arguments, 2);
|
223 | var argIndex = 0;
|
224 | throw new Error(
|
225 | 'Unirouter Assertion Failed: ' +
|
226 | format.replace(/%s/g, function() { return args[argIndex++]; })
|
227 | );
|
228 | }
|
229 | }
|
230 |
|
231 | function pathParts(path) {
|
232 | return path == '' ? [] : path.split('/')
|
233 | }
|
234 |
|
235 | function routeParts(route) {
|
236 | var split = route.split(/\s+/)
|
237 | var method = split[0]
|
238 | var path = split[1]
|
239 |
|
240 |
|
241 | assert(
|
242 | split.length == 2,
|
243 | "Route `%s` separates method and path with a single block of whitespace", route
|
244 | )
|
245 |
|
246 |
|
247 | assert(
|
248 | /^[A-Z]+$/.test(method),
|
249 | "Route `%s` starts with an UPPERCASE method", route
|
250 | )
|
251 |
|
252 |
|
253 | assert(
|
254 | !/\/{2,}/.test(path),
|
255 | "Path `%s` has no adjacent `/` characters: `%s`", path
|
256 | )
|
257 | assert(
|
258 | path[0] == '/',
|
259 | "Path `%s` must start with the `/` character", path
|
260 | )
|
261 | assert(
|
262 | path == '/' || !/\/$/.test(path),
|
263 | "Path `%s` does not end with the `/` character", path
|
264 | )
|
265 | assert(
|
266 | path.indexOf('#') === -1 && path.indexOf('?') === -1,
|
267 | "Path `%s` does not contain the `#` or `?` characters", path
|
268 | )
|
269 |
|
270 | return pathParts(path.slice(1)).concat(method)
|
271 | }
|
272 |
|
273 |
|
274 | function LookupTree() {
|
275 | this.tree = {}
|
276 | }
|
277 |
|
278 | function lookupTreeReducer(tree, part) {
|
279 | return tree && (tree[part] || tree[':'])
|
280 | }
|
281 |
|
282 | LookupTree.prototype.find = function(parts) {
|
283 | return (parts.reduce(lookupTreeReducer, this.tree) || {})['']
|
284 | }
|
285 |
|
286 | LookupTree.prototype.add = function(parts, route) {
|
287 | var i, branch
|
288 | var branches = parts.map(function(part) { return part[0] == ':' ? ':' : part })
|
289 | var currentTree = this.tree
|
290 |
|
291 | for (i = 0; i < branches.length; i++) {
|
292 | branch = branches[i]
|
293 | if (!currentTree[branch]) {
|
294 | currentTree[branch] = {}
|
295 | }
|
296 | currentTree = currentTree[branch]
|
297 | }
|
298 |
|
299 | assert(
|
300 | !currentTree[branch],
|
301 | "Path `%s` conflicts with another path", parts.join('/')
|
302 | )
|
303 |
|
304 | currentTree[''] = route
|
305 | }
|
306 |
|
307 |
|
308 | function createRouter(routes, aliases) {
|
309 | var parts, name, route;
|
310 | var routesParams = {};
|
311 | var lookupTree = new LookupTree;
|
312 |
|
313 |
|
314 | aliases = aliases || {};
|
315 |
|
316 |
|
317 | for (name in routes) {
|
318 | if (routes.hasOwnProperty(name)) {
|
319 | route = routes[name]
|
320 |
|
321 | assert(
|
322 | typeof route == 'string',
|
323 | "Route '%s' must be a string", name
|
324 | )
|
325 | assert(
|
326 | name.indexOf('.') == -1,
|
327 | "Route names must not contain the '.' character", name
|
328 | )
|
329 |
|
330 | parts = routeParts(route)
|
331 |
|
332 | routesParams[name] = parts
|
333 | .map(function(part, i) { return part[0] == ':' && [part.substr(1), i] })
|
334 | .filter(function(x) { return x })
|
335 |
|
336 | lookupTree.add(parts, name)
|
337 | }
|
338 | }
|
339 |
|
340 |
|
341 | for (route in aliases) {
|
342 | if (aliases.hasOwnProperty(route)) {
|
343 | name = aliases[route]
|
344 |
|
345 | assert(
|
346 | routes[name],
|
347 | "Alias from '%s' to non-existent route '%s'.", route, name
|
348 | )
|
349 |
|
350 | lookupTree.add(routeParts(route), name);
|
351 | }
|
352 | }
|
353 |
|
354 |
|
355 | return {
|
356 | lookup: function(uri, method) {
|
357 | method = method ? method.toUpperCase() : 'GET'
|
358 |
|
359 | var i, x
|
360 |
|
361 | var split = uri
|
362 |
|
363 | .replace(/^\/|\/($|\?)/g, '')
|
364 |
|
365 | .replace(/#.*$/, '')
|
366 | .split('?', 2)
|
367 |
|
368 | var parts = pathParts(split[0]).map(decodeURIComponent).concat(method)
|
369 | var name = lookupTree.find(parts)
|
370 | var options = {}
|
371 | var params, queryParts
|
372 |
|
373 | params = routesParams[name] || []
|
374 | queryParts = split[1] ? split[1].split('&') : []
|
375 |
|
376 | for (i = 0; i != queryParts.length; i++) {
|
377 | x = queryParts[i].split('=')
|
378 | options[x[0]] = decodeURIComponent(x[1])
|
379 | }
|
380 |
|
381 |
|
382 | for (i = 0; i != params.length; i++) {
|
383 | x = params[i]
|
384 | options[x[0]] = parts[x[1]]
|
385 | }
|
386 |
|
387 | return {name: name, options: options}
|
388 | },
|
389 |
|
390 |
|
391 | generate: function(name, options) {
|
392 | options = options || {}
|
393 |
|
394 | var params = routesParams[name] || []
|
395 | var paramNames = params.map(function(x) { return x[0]; })
|
396 | var route = routes[name]
|
397 | var query = []
|
398 | var inject = []
|
399 | var key
|
400 |
|
401 | assert(route, "No route with name `%s` exists", name)
|
402 |
|
403 | var path = route.split(' ')[1]
|
404 |
|
405 | for (key in options) {
|
406 | if (options.hasOwnProperty(key)) {
|
407 | if (paramNames.indexOf(key) === -1) {
|
408 | assert(
|
409 | /^[a-zA-Z0-9-_]+$/.test(key),
|
410 | "Non-route parameters must use only the following characters: A-Z, a-z, 0-9, -, _"
|
411 | )
|
412 |
|
413 | query.push(key+'='+encodeURIComponent(options[key]))
|
414 | }
|
415 | else {
|
416 | inject.push(key)
|
417 | }
|
418 | }
|
419 | }
|
420 |
|
421 | assert(
|
422 | inject.sort().join() == paramNames.slice(0).sort().join(),
|
423 | "You must specify options for all route params when using `uri`."
|
424 | )
|
425 |
|
426 | var uri =
|
427 | paramNames.reduce(function pathReducer(injected, key) {
|
428 | return injected.replace(':'+key, encodeURIComponent(options[key]))
|
429 | }, path)
|
430 |
|
431 | if (query.length) {
|
432 | uri += '?' + query.join('&')
|
433 | }
|
434 |
|
435 | return uri
|
436 | }
|
437 | };
|
438 | }
|
439 |
|
440 |
|
441 | if (typeof module !== 'undefined' && module.exports) {
|
442 | module.exports = createRouter
|
443 | }
|
444 | else {
|
445 | root.unirouter = createRouter
|
446 | }
|
447 | })(this);
|
448 |
|
449 | },{}],6:[function(require,module,exports){
|
450 |
|
451 |
|
452 |
|
453 |
|
454 |
|
455 |
|
456 |
|
457 |
|
458 |
|
459 | 'use strict'
|
460 |
|
461 |
|
462 |
|
463 |
|
464 |
|
465 |
|
466 |
|
467 |
|
468 |
|
469 |
|
470 |
|
471 |
|
472 |
|
473 |
|
474 |
|
475 | const https = require('https')
|
476 | const { URL } = require('url')
|
477 | const path = require('path')
|
478 | const querystring = require('querystring')
|
479 |
|
480 | const handlers = require('../lib/handlers.js')
|
481 | const wrapper = require('../lib/wrapper.js')
|
482 |
|
483 |
|
484 | function normaliseLambdaRequest (
|
485 | event /* : LambdaEvent */
|
486 | ) /* : BmRequest */ {
|
487 | const headers = wrapper.keysToLowerCase(event.headers)
|
488 | let body = event.body
|
489 | try {
|
490 | body = JSON.parse(body)
|
491 | } catch (e) {
|
492 |
|
493 | }
|
494 | const host = headers['x-forwarded-host'] || headers.host
|
495 | return {
|
496 | body,
|
497 | headers,
|
498 | method: wrapper.normaliseMethod(event.httpMethod),
|
499 | route: event.path,
|
500 | url: {
|
501 | host,
|
502 | hostname: host,
|
503 | params: {},
|
504 | pathname: event.path,
|
505 | protocol: wrapper.protocolFromHeaders(headers),
|
506 | query: event.queryStringParameters || {}
|
507 | }
|
508 | }
|
509 | }
|
510 |
|
511 | async function handler (
|
512 | event /* : LambdaEvent */,
|
513 | context /* : any */
|
514 | ) /* : Promise<APIGatewayResult> */ {
|
515 | const startTime = Date.now()
|
516 | context.callbackWaitsForEmptyEventLoop = false
|
517 |
|
518 | const request = normaliseLambdaRequest(event)
|
519 | const internalHeaders = {
|
520 | 'Content-Type': 'application/json'
|
521 | }
|
522 |
|
523 |
|
524 | const config = require(path.join(__dirname, 'bm-server.json'))
|
525 |
|
526 | const finish = (
|
527 | statusCode ,
|
528 | body ,
|
529 | customHeaders
|
530 | ) => {
|
531 | const headers = wrapper.keysToLowerCase(Object.assign(internalHeaders, customHeaders))
|
532 | const endTime = Date.now()
|
533 | const requestTime = endTime - startTime
|
534 |
|
535 | if (
|
536 | process.env.ONEBLINK_ANALYTICS_ORIGIN &&
|
537 | process.env.ONEBLINK_ANALYTICS_COLLECTOR_TOKEN
|
538 | ) {
|
539 | try {
|
540 | const token = process.env.ONEBLINK_ANALYTICS_COLLECTOR_TOKEN
|
541 | const hostname = new URL(process.env.ONEBLINK_ANALYTICS_ORIGIN).hostname
|
542 | const httpsRequest = https.request({
|
543 | hostname,
|
544 | path: '/events',
|
545 | method: 'POST',
|
546 | headers: {
|
547 | 'Content-Type': 'application/json',
|
548 | Authorization: `Bearer ${token}`
|
549 | }
|
550 | })
|
551 | httpsRequest.write(JSON.stringify({
|
552 | events: [
|
553 | {
|
554 | name: 'Server CLI Request',
|
555 | date: new Date().toISOString(),
|
556 | tags: {
|
557 | env: config.env,
|
558 | scope: config.scope,
|
559 | request: {
|
560 | method: request.method.toUpperCase(),
|
561 | query: request.url.query,
|
562 | port: 443,
|
563 | path: request.route,
|
564 | hostName: request.url.hostname,
|
565 | params: request.url.params,
|
566 | protocol: request.url.protocol
|
567 | },
|
568 | response: {
|
569 | statusCode: statusCode
|
570 | },
|
571 | requestTime: {
|
572 | startDateTime: new Date(startTime).toISOString(),
|
573 | startTimeStamp: startTime,
|
574 | endDateTime: new Date(endTime).toISOString(),
|
575 | endTimeStamp: endTime,
|
576 | ms: requestTime,
|
577 | s: requestTime / 1000
|
578 | }
|
579 | }
|
580 | }
|
581 | ]
|
582 | }))
|
583 | httpsRequest.end()
|
584 | } catch (e) {
|
585 | console.warn('An error occurred attempting to POST analytics event', e)
|
586 | }
|
587 | }
|
588 |
|
589 | let path = request.url.pathname
|
590 | const search = querystring.stringify(request.url.query)
|
591 | if (search) {
|
592 | path += `?${search}`
|
593 | }
|
594 | let referrer = request.headers.referrer
|
595 | if (typeof referrer !== 'string' || !referrer) {
|
596 | referrer = '-'
|
597 | }
|
598 | let userAgent = request.headers['user-agent']
|
599 | if (typeof userAgent !== 'string' || !userAgent) {
|
600 | userAgent = '-'
|
601 | }
|
602 | console.log(`${request.method.toUpperCase()} ${path}${querystring.stringify(request.url.query)} ${statusCode} "${requestTime} ms" "${referrer}" "${userAgent}"`)
|
603 |
|
604 | const result = {
|
605 | headers: headers,
|
606 | statusCode: statusCode
|
607 | }
|
608 | if (body !== undefined) {
|
609 | result.body = typeof body === 'string' ? body : JSON.stringify(body)
|
610 | }
|
611 | return result
|
612 | }
|
613 |
|
614 | try {
|
615 |
|
616 | let routeConfig
|
617 | try {
|
618 | routeConfig = handlers.findRouteConfig(event.path, config.routes)
|
619 | request.url.params = routeConfig.params || {}
|
620 | request.route = routeConfig.route
|
621 | } catch (error) {
|
622 | return finish(404, {
|
623 | error: 'Not Found',
|
624 | message: error.message,
|
625 | statusCode: 404
|
626 | })
|
627 | }
|
628 |
|
629 |
|
630 | if (request.headers.origin) {
|
631 | if (!config.cors) {
|
632 |
|
633 | return finish(405, {
|
634 | error: 'Method Not Allowed',
|
635 | message: 'OPTIONS method has not been implemented',
|
636 | statusCode: 405
|
637 | })
|
638 | }
|
639 | if (!config.cors.origins.some((origin) => origin === '*' || origin === request.headers.origin)) {
|
640 |
|
641 | return finish(200)
|
642 | }
|
643 |
|
644 | internalHeaders['Access-Control-Allow-Origin'] = request.headers.origin
|
645 | internalHeaders['Access-Control-Expose-Headers'] = config.cors.exposedHeaders.join(',')
|
646 |
|
647 | if (request.method === 'options' && request.headers['access-control-request-method']) {
|
648 | internalHeaders['Access-Control-Allow-Headers'] = config.cors.headers.join(',')
|
649 | internalHeaders['Access-Control-Allow-Methods'] = request.headers['access-control-request-method']
|
650 | internalHeaders['Access-Control-Max-Age'] = config.cors.maxAge
|
651 | }
|
652 |
|
653 | if (config.cors.credentials) {
|
654 | internalHeaders['Access-Control-Allow-Credentials'] = true
|
655 | }
|
656 | }
|
657 | if (request.method === 'options') {
|
658 |
|
659 |
|
660 | return finish(200)
|
661 | }
|
662 |
|
663 |
|
664 |
|
665 | const projectPath = path.join(__dirname, 'project')
|
666 | if (process.cwd() !== projectPath) {
|
667 | try {
|
668 | process.chdir(projectPath)
|
669 | } catch (err) {
|
670 | throw new Error(`Could not change current working directory to '${projectPath}': ${err}`)
|
671 | }
|
672 | }
|
673 |
|
674 | const handler = await handlers.getHandler(path.join(__dirname, routeConfig.module), request.method)
|
675 | if (typeof handler !== 'function') {
|
676 | return finish(405, {
|
677 | error: 'Method Not Allowed',
|
678 | message: `${request.method.toUpperCase()} method has not been implemented`,
|
679 | statusCode: 405
|
680 | })
|
681 | }
|
682 |
|
683 | const response = await handlers.executeHandler(handler, request)
|
684 | return finish(response.statusCode, response.payload, response.headers)
|
685 | } catch (error) {
|
686 | if (error && error.isBoom && error.output && error.output.payload && error.output.statusCode) {
|
687 | if (error.data) {
|
688 | console.error(error, JSON.stringify(error.data))
|
689 | } else {
|
690 | console.error(error)
|
691 | }
|
692 | return finish(error.output.statusCode, error.output.payload, error.output.headers)
|
693 | }
|
694 |
|
695 | console.error(error)
|
696 | return finish(500, {
|
697 | error: 'Internal Server Error',
|
698 | message: 'An internal server error occurred',
|
699 | statusCode: 500
|
700 | })
|
701 | }
|
702 | }
|
703 |
|
704 | module.exports = {
|
705 | handler,
|
706 | normaliseLambdaRequest
|
707 | }
|
708 |
|
709 |
|
710 | },{"../lib/handlers.js":2,"../lib/wrapper.js":4,"https":undefined,"path":undefined,"querystring":undefined,"url":undefined}]},{},[6])(6)
|
711 | });
|