UNPKG

19.8 kBJavaScriptView Raw
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/* @flow */
3'use strict'
4
5const createInternal = require('./utils/internal.js').createInternal
6
7/* ::
8import type {Headers} from '../types.js'
9*/
10
11const internal = createInternal()
12
13class 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 /* : string */,
36 value /* : string */
37 ) /* : BmResponse */ {
38 key = key.toLowerCase()
39 internal(this).headers[key] = value
40 return this
41 }
42
43 setPayload (
44 payload /* : any */
45 ) /* : BmResponse */ {
46 internal(this).payload = payload
47 return this
48 }
49
50 setStatusCode (
51 code /* : number */
52 ) /* : BmResponse */ {
53 internal(this).statusCode = code
54 return this
55 }
56}
57
58module.exports = BmResponse
59
60},{"./utils/internal.js":3}],2:[function(require,module,exports){
61/* @flow */
62'use strict'
63
64/* ::
65import type {
66 Handler,
67 BmRequest,
68 RouteConfiguration
69} from '../types.js'
70*/
71
72const uniloc = require('uniloc')
73
74const BmResponse = require('../lib/bm-response.js')
75
76function 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 // If a result has been returned:
85 // try and set status code or
86 // try and set payload
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
98function getHandler (
99 module /* : string */,
100 method /* : string */
101) /* : Promise<Handler | void> */ {
102 try {
103 // $FlowIssue in this case, we explicitly `require()` dynamically
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
114function 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
134module.exports = {
135 findRouteConfig,
136 executeHandler,
137 getHandler
138}
139
140},{"../lib/bm-response.js":1,"uniloc":5}],3:[function(require,module,exports){
141/* @flow */
142
143/* :: import type { MapObject } from '../../types.js' */
144
145// https://developer.mozilla.org/en-US/Add-ons/SDK/Guides/Contributor_s_Guide/Private_Properties
146
147// simpler than alternative: https://www.npmjs.com/package/namespace
148
149function createInternal () {
150 const map /* : WeakMap<Object, MapObject> */ = new WeakMap()
151 return (object /* : Object */) /* : MapObject */ => {
152 const values = map.get(object) || {}
153 if (!map.has(object)) {
154 map.set(object, values)
155 }
156 return values
157 }
158}
159
160module.exports = {
161 createInternal
162}
163
164},{}],4:[function(require,module,exports){
165/* @flow */
166'use strict'
167
168/* ::
169import type {
170 BmRequest,
171 Headers,
172 MapObject,
173 Protocol
174} from '../types'
175*/
176
177function 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
186function normaliseMethod (
187 method /* : string */
188) /* : string */ {
189 return method.toLowerCase()
190}
191
192/**
193https://www.w3.org/TR/url-1/#dom-urlutils-protocol
194protocol ends with ':', same as in Node.js 'url' module
195https://en.wikipedia.org/wiki/List_of_HTTP_header_fields
196*/
197function 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
212module.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 // Validate route format
241 assert(
242 split.length == 2,
243 "Route `%s` separates method and path with a single block of whitespace", route
244 )
245
246 // Validate method format
247 assert(
248 /^[A-Z]+$/.test(method),
249 "Route `%s` starts with an UPPERCASE method", route
250 )
251
252 // Validate path format
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 // By default, there are no aliases
314 aliases = aliases || {};
315
316 // Copy routes into lookup tree
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 // Copy aliases into lookup tree
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 // Strip leading and trailing '/' (at end or before query string)
363 .replace(/^\/|\/($|\?)/g, '')
364 // Strip fragment identifiers
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 // Named parameters overwrite query parameters
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/* eslint-disable no-console */
451
452/**
453This module exports a "handler" function,
454that wraps a customer function.
455We bundle this module and its dependencies to ../dist/wrapper.js .
456To bundle: `npm run build`
457*/
458/* @flow */
459'use strict'
460
461/* ::
462import type {
463 BmRequest,
464 Headers,
465 LambdaEvent
466} from '../types.js'
467
468type APIGatewayResult = {
469 statusCode: number,
470 headers: Headers,
471 body?: string
472}
473*/
474
475const https = require('https')
476const { URL } = require('url')
477const path = require('path')
478const querystring = require('querystring')
479
480const handlers = require('../lib/handlers.js')
481const wrapper = require('../lib/wrapper.js')
482
483// return only the pertinent data from a API Gateway + Lambda event
484function 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 // Do nothing...
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
511async 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 /* : Headers */ = {
520 'Content-Type': 'application/json'
521 }
522
523 // $FlowFixMe requiring file without string literal to accommodate for __dirname
524 const config = require(path.join(__dirname, 'bm-server.json'))
525
526 const finish = (
527 statusCode /* : number */,
528 body /* : mixed | void */,
529 customHeaders /* : Headers | void */
530 ) /* : APIGatewayResult */ => {
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 /* : APIGatewayResult */ = {
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 // Get handler module based on route
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 // Check for browser requests and apply CORS if required
630 if (request.headers.origin) {
631 if (!config.cors) {
632 // No cors, we will return 405 result and let browser handler error
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 // Invalid origin, we will return 200 result and let browser handler error
641 return finish(200)
642 }
643 // Headers for all cross origin requests
644 internalHeaders['Access-Control-Allow-Origin'] = request.headers.origin
645 internalHeaders['Access-Control-Expose-Headers'] = config.cors.exposedHeaders.join(',')
646 // Headers for OPTIONS cross origin requests
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 // Only set credentials header if truthy
653 if (config.cors.credentials) {
654 internalHeaders['Access-Control-Allow-Credentials'] = true
655 }
656 }
657 if (request.method === 'options') {
658 // For OPTIONS requests, we can just finish
659 // as we have created our own implementation of CORS
660 return finish(200)
661 }
662
663 // Change current working directory to the project
664 // to accommodate for packages using process.cwd()
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
704module.exports = {
705 handler,
706 normaliseLambdaRequest
707}
708/* eslint-enable no-console */
709
710},{"../lib/handlers.js":2,"../lib/wrapper.js":4,"https":undefined,"path":undefined,"querystring":undefined,"url":undefined}]},{},[6])(6)
711});