From c4811cac25e3f6eca0d80503bf5b27a2ba2b3fc0 Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Thu, 9 May 2019 00:40:32 +0300 Subject: [PATCH 01/71] demo websoket version --- package.json | 21 +- src/index.js | 1055 ++++++++++++++++++++++++++++---------------------- 2 files changed, 608 insertions(+), 468 deletions(-) diff --git a/package.json b/package.json index 491fe4530..ce4ed640f 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "serverless-offline", - "version": "4.10.0", + "version": "4.9.3", "description": "Emulate AWS λ and API Gateway locally when developing your Serverless project", "license": "MIT", "main": "src/index.js", "scripts": { "test": "mocha test", + "test-only": "mocha ", "lint": "eslint src/**/*.js test/**/*.js" }, "repository": { @@ -112,7 +113,8 @@ "Bob Thomas (https://github.com/bob-thomas)", "Alessandro Palumbo (https://github.com/apalumbo)", "Selcuk Cihan (https://github.com/selcukcihan)", - "G Roques (https://github.com/gbroques)" + "G Roques (https://github.com/gbroques)", + "Ram Hardy (https://github.com/computerpunc" ], "dependencies": { "boom": "^7.3.0", @@ -120,19 +122,22 @@ "h2o2": "^6.1.0", "hapi": "^16.7.0", "hapi-cors-headers": "^1.0.3", + "hapi-plugin-websocket": "^1.2.19", "js-string-escape": "^1.0.1", "jsonpath-plus": "^0.16.0", "jsonschema": "^1.2.4", - "jsonwebtoken": "^8.5.1", + "jsonwebtoken": "^8.5.0", "trim-newlines": "^2.0.0", - "velocityjs": "^1.1.3" + "velocityjs": "^1.1.3", + "ws": "^6.2.1" }, "devDependencies": { "chai": "^4.2.0", "dirty-chai": "^2.0.1", - "eslint": "^5.16.0", - "eslint-config-dherault": "^1.0.2", - "mocha": "^6.1.4", - "sinon": "^7.3.2" + "eslint": "^5.15.1", + "eslint-config-nelson": "^0.2.0", + "eslint-plugin-import": "^2.16.0", + "mocha": "^6.0.2", + "sinon": "^7.2.7" } } diff --git a/src/index.js b/src/index.js index 1f15e20af..299e1de6e 100644 --- a/src/index.js +++ b/src/index.js @@ -5,7 +5,6 @@ const { exec } = require('child_process'); // External dependencies const Hapi = require('hapi'); -const h2o2 = require('h2o2'); const corsHeaders = require('hapi-cors-headers'); const crypto = require('crypto'); @@ -36,6 +35,7 @@ class Offline { this.serverlessLog = serverless.cli.log.bind(serverless.cli); this.options = options; this.exitCode = 0; + this.clients = new Map; this.commands = { offline: { @@ -91,10 +91,6 @@ class Offline { usage: 'Disable the timeout feature.', shortcut: 't', }, - binPath: { - usage: 'Path to the Serverless binary.', - shortcut: 'b', - }, noEnvironment: { usage: 'Turns off loading of your environment variables from serverless.yml. Allows the usage of tools such as PM2 or docker-compose.', }, @@ -321,7 +317,8 @@ class Offline { }, }); - this.server.register(h2o2, err => err && this.serverlessLog(err)); + this.server.register(require('h2o2'), err => err && this.serverlessLog(err)); + const connectionOptions = { host: this.options.host, @@ -351,6 +348,9 @@ class Offline { // Passes the configuration object to the server this.server.connection(connectionOptions); + // Register WebSocket plugin + this.server.register(require('hapi-plugin-websocket'), err => err && this.serverlessLog(err)); + // Enable CORS preflight response this.server.ext('onPreResponse', corsHeaders); } @@ -397,6 +397,7 @@ class Offline { } } + const serviceInfo={serviceRuntime, defaultContentType, apiKeys, protectedRoutes, wsActions:{}}; Object.keys(this.service.functions).forEach(key => { const fun = this.service.getFunction(key); @@ -410,565 +411,692 @@ class Offline { this.serverlessLog(`Routes for ${funName}:`); // Adds a route for each http endpoint - // eslint-disable-next-line (fun.events && fun.events.length || this.serverlessLog('(none)')) && fun.events.forEach(event => { - if (!event.http) return this.serverlessLog('(none)'); - - // Handle Simple http setup, ex. - http: GET users/index - if (typeof event.http === 'string') { - const split = event.http.split(' '); - event.http = { - path: split[1], - method: split[0], - }; - } + // console.log(event) + if (event.http) this._createHttpEndpoint(serviceInfo, fun, funName, servicePath, funOptions, event); + else if (event.websocket) this._createWsEndpoint(serviceInfo, fun, funName, servicePath, funOptions, event); + else this.serverlessLog('(none)'); + }); + }); - // generate an enpoint via the endpoint class - const endpoint = new Endpoint(event.http, funOptions).generate(); + console.log(serviceInfo.wsActions); - const integration = endpoint.integration || 'lambda-proxy'; - const requestBodyValidationModel = (['lambda', 'lambda-proxy'].includes(integration) - ? requestBodyValidator.getModel(this.service.custom, event.http, this.serverlessLog) - : null); - const epath = endpoint.path; - const method = endpoint.method.toUpperCase(); - const requestTemplates = endpoint.requestTemplates; + const plugin=this; + const ApiGatewayManagementApi=class { + constructor(apiVersion, client) { - // Prefix must start and end with '/' BUT path must not end with '/' - let fullPath = this.options.prefix + (epath.startsWith('/') ? epath.slice(1) : epath); - if (fullPath !== '/' && fullPath.endsWith('/')) fullPath = fullPath.slice(0, -1); - fullPath = fullPath.replace(/\+}/g, '*}'); + } - if (event.http.private) { - protectedRoutes.push(`${method}#${fullPath}`); + getByValue(map, searchValue) { + for (let [key, value] of map.entries()) { + if (value === searchValue) + return key; } + return undefined; + } - this.serverlessLog(`${method} ${fullPath}${requestBodyValidationModel && !this.options.disableModelValidation ? ` - request body will be validated against ${requestBodyValidationModel.name}` : ''}`); - - // If the endpoint has an authorization function, create an authStrategy for the route - const authStrategyName = this.options.noAuth ? null : this._configureAuthorization(endpoint, funName, method, epath, servicePath, serviceRuntime); + postToConnection({ConnectionId,Data}) { + const ws=this.getByValue(plugin.clients, ConnectionId); + if (!ws||!Data) return {promise:()=>{ return new Promise((resolve, reject)=>{setTimeout(()=>{reject()}, 10)})}}; + ws.send(Data); + return {promise:()=>{ return new Promise((resolve, reject)=>{setTimeout(()=>{resolve()}, 10)})}}; + } + }; - let cors = null; - if (endpoint.cors) { - cors = { - origin: endpoint.cors.origins || this.options.corsConfig.origin, - headers: endpoint.cors.headers || this.options.corsConfig.headers, - credentials: endpoint.cors.credentials || this.options.corsConfig.credentials, - exposedHeaders: this.options.corsConfig.exposedHeaders, - }; + const doAction=(ws, connectionId, name, event, doDeafultAction, onError)=>{ + let action=serviceInfo.wsActions[name]; + if (!action&&doDeafultAction) action=serviceInfo.wsActions['$default']; + if (!action) return; + action.handler(event, {API:{ApiGatewayManagementApi}}, ()=>{}).catch(err=>ws.send(JSON.stringify({message:'Internal server error', connectionId, requestId:"1234567890"}))); + }; + + this.server.route({ + method: "POST", path: "/dev", + config: { + payload: { output: "data", parse: true, allow: "application/json" }, + // auth: { mode: "required", strategy: "basic" }, + plugins: { + websocket: { + only: true, + initially: true, + // subprotocol: "quux/1.0", + connect: ({ ctx, ws }) => { + // ctx.to = setInterval(() => { + // ws.send(JSON.stringify({ cmd: "PING" })) + // }, 5000); + const connectionId=utils.randomId(); + console.log('connect '+connectionId); + this.clients.set(ws, connectionId); + + doAction(ws, connectionId, '$connect', {requestContext:{eventType:'CONNECT', connectionId}}); + }, + disconnect: ({ ctx, ws }) => { + // if (ctx.to !== null) { + // clearTimeout(ctx.to) + // ctx.to = null + // } + const connectionId=this.clients.get(ws); + console.log('disconnect '+connectionId); + this.clients.delete(ws); + + doAction(ws, connectionId, '$disconnect', {requestContext:{eventType:'DISCONNECT', connectionId}}); + } + } } + }, + handler: (request, reply) => { + const { initially, ws } = request.websocket(); + console.log(`initially:${initially}`) + if (!request.payload||initially) return; + const connectionId=this.clients.get(ws); + doAction(ws, connectionId, request.payload.action||'$default', {body:JSON.stringify(request.payload), requestContext:{domainName:'localhost', stage:'local', connectionId}}, true); - // Route creation - const routeMethod = method === 'ANY' ? '*' : method; + //return reply().code(204); + } + }); + } - const state = this.options.disableCookieValidation ? { - parse: false, - failAction: 'ignore', - } : { - parse: true, - failAction: 'error', + _createWsEndpoint(serviceInfo, fun, funName, servicePath, funOptions, event) { + // console.log('serviceInfo:'); + // console.log(serviceInfo); + // console.log('fun:'); + // console.log(fun); + // console.log('funName:'); + // console.log(funName); + // console.log('servicePath:'); + // console.log(servicePath); + // console.log('funOptions:'); + // console.log(funOptions); + // console.log('event:'); + // console.log(event); + + let handler; // The lambda function + Object.assign(process.env, this.originalEnvironment); + + try { + if (this.options.noEnvironment) { + // This evict errors in server when we use aws services like ssm + const baseEnvironment = { + AWS_REGION: 'dev', }; + if (!process.env.AWS_PROFILE) { + baseEnvironment.AWS_ACCESS_KEY_ID = 'dev'; + baseEnvironment.AWS_SECRET_ACCESS_KEY = 'dev'; + } - const routeConfig = { - cors, - auth: authStrategyName, - timeout: { socket: false }, - state, - }; + process.env = Object.assign(baseEnvironment, process.env); + } + else { + Object.assign( + process.env, + { AWS_REGION: this.service.provider.region }, + this.service.provider.environment, + this.service.functions[funName].environment + ); + } + process.env._HANDLER = fun.handler; + handler = functionHelper.createHandler(funOptions, this.options); + } + catch (err) { + return this.serverlessLog(`Error while loading ${funName}`, err); + } - // skip HEAD routes as hapi will fail with 'Method name not allowed: HEAD ...' - // for more details, check https://github.com/dherault/serverless-offline/issues/204 - if (routeMethod === 'HEAD') { - this.serverlessLog('HEAD method event detected. Skipping HAPI server route mapping ...'); + const actionName=event.websocket.route; + const action={funName, fun, funOptions, servicePath, handler}; + serviceInfo.wsActions[actionName]=action; + } - return; - } + _createHttpEndpoint(serviceInfo, fun, funName, servicePath, funOptions, event) { + // Handle Simple http setup, ex. - http: GET users/index + if (typeof event.http === 'string') { + const split = event.http.split(' '); + event.http = { + path: split[1], + protectedRoutes: split[0], + }; + } - if (routeMethod !== 'HEAD' && routeMethod !== 'GET') { - // maxBytes: Increase request size from 1MB default limit to 10MB. - // Cf AWS API GW payload limits. - routeConfig.payload = { parse: false, maxBytes: 1024 * 1024 * 10 }; - } + // generate an enpoint via the endpoint class + const endpoint = new Endpoint(event.http, funOptions).generate(); - this.server.route({ - method: routeMethod, - path: fullPath, - config: routeConfig, - handler: (request, reply) => { // Here we go - // Payload processing - const encoding = utils.detectEncoding(request); - - request.payload = request.payload && request.payload.toString(encoding); - request.rawPayload = request.payload; - - // Headers processing - // Hapi lowercases the headers whereas AWS does not - // so we recreate a custom headers object from the raw request - const headersArray = request.raw.req.rawHeaders; - - // During tests, `server.inject` uses *shot*, a package - // for performing injections that does not entirely mimick - // Hapi's usual request object. rawHeaders are then missing - // Hence the fallback for testing - - // Normal usage - if (headersArray) { - request.unprocessedHeaders = {}; - request.multiValueHeaders = {}; - - for (let i = 0; i < headersArray.length; i += 2) { - request.unprocessedHeaders[headersArray[i]] = headersArray[i + 1]; - request.multiValueHeaders[headersArray[i]] = (request.multiValueHeaders[headersArray[i]] || []).concat(headersArray[i + 1]); - } - } - // Lib testing - else { - request.unprocessedHeaders = request.headers; - } + const integration = endpoint.integration || 'lambda-proxy'; + const requestBodyValidationModel = (['lambda', 'lambda-proxy'].includes(integration) + ? requestBodyValidator.getModel(this.service.custom, event.http, this.serverlessLog) + : null); + const epath = endpoint.path; + const method = endpoint.method.toUpperCase(); + const requestTemplates = endpoint.requestTemplates; - // Incomming request message - this.printBlankLine(); - this.serverlessLog(`${method} ${request.path} (λ: ${funName})`); + // Prefix must start and end with '/' BUT path must not end with '/' + let fullPath = this.options.prefix + (epath.startsWith('/') ? epath.slice(1) : epath); + if (fullPath !== '/' && fullPath.endsWith('/')) fullPath = fullPath.slice(0, -1); + fullPath = fullPath.replace(/\+}/g, '*}'); - // Check for APIKey - if ((protectedRoutes.includes(`${routeMethod}#${fullPath}`) || protectedRoutes.includes(`ANY#${fullPath}`)) && !this.options.noAuth) { - const errorResponse = response => response({ message: 'Forbidden' }).code(403).type('application/json').header('x-amzn-ErrorType', 'ForbiddenException'); + if (event.http.private) { + serviceInfo.protectedRoutes.push(`${method}#${fullPath}`); + } - if ('x-api-key' in request.headers) { - const requestToken = request.headers['x-api-key']; - if (requestToken !== this.options.apiKey) { - debugLog(`Method ${method} of function ${funName} token ${requestToken} not valid`); + this.serverlessLog(`${method} ${fullPath}${requestBodyValidationModel && !this.options.disableModelValidation ? ` - request body will be validated against ${requestBodyValidationModel.name}` : ''}`); - return errorResponse(reply); - } - } - else if (request.auth && request.auth.credentials && 'usageIdentifierKey' in request.auth.credentials) { - const usageIdentifierKey = request.auth.credentials.usageIdentifierKey; - if (usageIdentifierKey !== this.options.apiKey) { - debugLog(`Method ${method} of function ${funName} token ${usageIdentifierKey} not valid`); + // If the endpoint has an authorization function, create an authStrategy for the route + const authStrategyName = this.options.noAuth ? null : this._configureAuthorization(endpoint, funName, method, epath, servicePath, serviceInfo.serviceRuntime); - return errorResponse(reply); - } - } - else { - debugLog(`Missing x-api-key on private function ${funName}`); + let cors = null; + if (endpoint.cors) { + cors = { + origin: endpoint.cors.origins || this.options.corsConfig.origin, + headers: endpoint.cors.headers || this.options.corsConfig.headers, + credentials: endpoint.cors.credentials || this.options.corsConfig.credentials, + exposedHeaders: this.options.corsConfig.exposedHeaders, + }; + } - return errorResponse(reply); - } - } - // Shared mutable state is the root of all evil they say - const requestId = utils.randomId(); - this.requests[requestId] = { done: false }; - this.currentRequestId = requestId; - - // Holds the response to do async op - const response = reply.response().hold(); - const contentType = request.mime || defaultContentType; - - // default request template to '' if we don't have a definition pushed in from serverless or endpoint - const requestTemplate = typeof requestTemplates !== 'undefined' && integration === 'lambda' ? requestTemplates[contentType] : ''; - - // https://hapijs.com/api#route-configuration doesn't seem to support selectively parsing - // so we have to do it ourselves - const contentTypesThatRequirePayloadParsing = ['application/json', 'application/vnd.api+json']; - if (contentTypesThatRequirePayloadParsing.includes(contentType)) { - try { - request.payload = JSON.parse(request.payload); - } - catch (err) { - debugLog('error in converting request.payload to JSON:', err); - } - } + // Route creation + const routeMethod = method === 'ANY' ? '*' : method; - debugLog('requestId:', requestId); - debugLog('contentType:', contentType); - debugLog('requestTemplate:', requestTemplate); - debugLog('payload:', request.payload); + const state = this.options.disableCookieValidation ? { + parse: false, + failAction: 'ignore', + } : { + parse: true, + failAction: 'error', + }; - /* HANDLER LAZY LOADING */ + const routeConfig = { + cors, + auth: authStrategyName, + timeout: { socket: false }, + state, + }; - let handler; // The lambda function - Object.assign(process.env, this.originalEnvironment); + // skip HEAD routes as hapi will fail with 'Method name not allowed: HEAD ...' + // for more details, check https://github.com/dherault/serverless-offline/issues/204 + if (routeMethod === 'HEAD') { + this.serverlessLog('HEAD method event detected. Skipping HAPI server route mapping ...'); - try { - if (this.options.noEnvironment) { - // This evict errors in server when we use aws services like ssm - const baseEnvironment = { - AWS_REGION: 'dev', - }; - if (!process.env.AWS_PROFILE) { - baseEnvironment.AWS_ACCESS_KEY_ID = 'dev'; - baseEnvironment.AWS_SECRET_ACCESS_KEY = 'dev'; - } + return; + } - process.env = Object.assign(baseEnvironment, process.env); - } - else { - Object.assign( - process.env, - { AWS_REGION: this.service.provider.region }, - this.service.provider.environment, - this.service.functions[key].environment - ); - } - process.env._HANDLER = fun.handler; - handler = functionHelper.createHandler(funOptions, this.options); - } - catch (err) { - return this._reply500(response, `Error while loading ${funName}`, err); - } + if (routeMethod !== 'HEAD' && routeMethod !== 'GET') { + // maxBytes: Increase request size from 1MB default limit to 10MB. + // Cf AWS API GW payload limits. + routeConfig.payload = { parse: false, maxBytes: 1024 * 1024 * 10 }; + } - /* REQUEST TEMPLATE PROCESSING (event population) */ + this.server.route({ + method: routeMethod, + path: fullPath, + config: routeConfig, + handler: (request, reply) => { // Here we go + // Payload processing + const encoding = utils.detectEncoding(request); + + request.payload = request.payload && request.payload.toString(encoding); + request.rawPayload = request.payload; + + // Headers processing + // Hapi lowercases the headers whereas AWS does not + // so we recreate a custom headers object from the raw request + const headersArray = request.raw.req.rawHeaders; + + // During tests, `server.inject` uses *shot*, a package + // for performing injections that does not entirely mimick + // Hapi's usual request object. rawHeaders are then missing + // Hence the fallback for testing + + // Normal usage + if (headersArray) { + request.unprocessedHeaders = {}; + request.multiValueHeaders = {}; + + for (let i = 0; i < headersArray.length; i += 2) { + request.unprocessedHeaders[headersArray[i]] = headersArray[i + 1]; + request.multiValueHeaders[headersArray[i]] = (request.multiValueHeaders[headersArray[i]] || []).concat(headersArray[i + 1]); + } + } + // Lib testing + else { + request.unprocessedHeaders = request.headers; + } - let event = {}; + // Incomming request message + this.printBlankLine(); + this.serverlessLog(`${method} ${request.path} (λ: ${funName})`); - if (integration === 'lambda') { - if (requestTemplate) { - try { - debugLog('_____ REQUEST TEMPLATE PROCESSING _____'); - // Velocity templating language parsing - const velocityContext = createVelocityContext(request, this.velocityContextOptions, request.payload || {}); - event = renderVelocityTemplateObject(requestTemplate, velocityContext); - } - catch (err) { - return this._reply500(response, `Error while parsing template "${contentType}" for ${funName}`, err); - } - } - else if (typeof request.payload === 'object') { - event = request.payload || {}; - } + // Check for APIKey + if ((serviceInfo.protectedRoutes.includes(`${routeMethod}#${fullPath}`) || serviceInfo.protectedRoutes.includes(`ANY#${fullPath}`)) && !this.options.noAuth) { + const errorResponse = response => response({ message: 'Forbidden' }).code(403).type('application/json').header('x-amzn-ErrorType', 'ForbiddenException'); + + if ('x-api-key' in request.headers) { + const requestToken = request.headers['x-api-key']; + if (requestToken !== this.options.apiKey) { + debugLog(`Method ${method} of function ${funName} token ${requestToken} not valid`); + + return errorResponse(reply); + } + } + else if (request.auth && request.auth.credentials && 'usageIdentifierKey' in request.auth.credentials) { + const usageIdentifierKey = request.auth.credentials.usageIdentifierKey; + if (usageIdentifierKey !== this.options.apiKey) { + debugLog(`Method ${method} of function ${funName} token ${usageIdentifierKey} not valid`); + + return errorResponse(reply); } - else if (integration === 'lambda-proxy') { - event = createLambdaProxyContext(request, this.options, this.velocityContextOptions.stageVariables); + } + else { + debugLog(`Missing x-api-key on private function ${funName}`); + + return errorResponse(reply); + } + } + // Shared mutable state is the root of all evil they say + const requestId = utils.randomId(); + this.requests[requestId] = { done: false }; + this.currentRequestId = requestId; + + // Holds the response to do async op + const response = reply.response().hold(); + const contentType = request.mime || serviceInfo.defaultContentType; + + // default request template to '' if we don't have a definition pushed in from serverless or endpoint + const requestTemplate = typeof requestTemplates !== 'undefined' && integration === 'lambda' ? requestTemplates[contentType] : ''; + + // https://hapijs.com/api#route-configuration doesn't seem to support selectively parsing + // so we have to do it ourselves + const contentTypesThatRequirePayloadParsing = ['application/json', 'application/vnd.api+json']; + if (contentTypesThatRequirePayloadParsing.includes(contentType)) { + try { + request.payload = JSON.parse(request.payload); + } + catch (err) { + debugLog('error in converting request.payload to JSON:', err); + } + } + + debugLog('requestId:', requestId); + debugLog('contentType:', contentType); + debugLog('requestTemplate:', requestTemplate); + debugLog('payload:', request.payload); + + /* HANDLER LAZY LOADING */ + + let handler; // The lambda function + Object.assign(process.env, this.originalEnvironment); + + try { + if (this.options.noEnvironment) { + // This evict errors in server when we use aws services like ssm + const baseEnvironment = { + AWS_REGION: 'dev', + }; + if (!process.env.AWS_PROFILE) { + baseEnvironment.AWS_ACCESS_KEY_ID = 'dev'; + baseEnvironment.AWS_SECRET_ACCESS_KEY = 'dev'; } - event.isOffline = true; + process.env = Object.assign(baseEnvironment, process.env); + } + else { + Object.assign( + process.env, + { AWS_REGION: this.service.provider.region }, + this.service.provider.environment, + this.service.functions[funName].environment + ); + } + process.env._HANDLER = fun.handler; + handler = functionHelper.createHandler(funOptions, this.options); + } + catch (err) { + return this._reply500(response, `Error while loading ${funName}`, err, requestId); + } + + /* REQUEST TEMPLATE PROCESSING (event population) */ - if (this.serverless.service.custom && this.serverless.service.custom.stageVariables) { - event.stageVariables = this.serverless.service.custom.stageVariables; + let event = {}; + + if (integration === 'lambda') { + if (requestTemplate) { + try { + debugLog('_____ REQUEST TEMPLATE PROCESSING _____'); + // Velocity templating language parsing + const velocityContext = createVelocityContext(request, this.velocityContextOptions, request.payload || {}); + event = renderVelocityTemplateObject(requestTemplate, velocityContext); } - else if (integration !== 'lambda-proxy') { - event.stageVariables = {}; + catch (err) { + return this._reply500(response, `Error while parsing template "${contentType}" for ${funName}`, err, requestId); } + } + else if (typeof request.payload === 'object') { + event = request.payload || {}; + } + } + else if (integration === 'lambda-proxy') { + event = createLambdaProxyContext(request, this.options, this.velocityContextOptions.stageVariables); + } + + event.isOffline = true; + + if (this.serverless.service.custom && this.serverless.service.custom.stageVariables) { + event.stageVariables = this.serverless.service.custom.stageVariables; + } + else if (integration !== 'lambda-proxy') { + event.stageVariables = {}; + } - debugLog('event:', event); + debugLog('event:', event); - // We create the context, its callback (context.done/succeed/fail) will send the HTTP response - const lambdaContext = createLambdaContext(fun, this.service.provider, (err, data, fromPromise) => { - // Everything in this block happens once the lambda function has resolved - debugLog('_____ HANDLER RESOLVED _____'); + // We create the context, its callback (context.done/succeed/fail) will send the HTTP response + const lambdaContext = createLambdaContext(fun, (err, data, fromPromise) => { + // Everything in this block happens once the lambda function has resolved + debugLog('_____ HANDLER RESOLVED _____'); - // User should not call context.done twice - if (this.requests[requestId].done) { - this.printBlankLine(); - const warning = fromPromise - ? `Warning: handler '${funName}' returned a promise and also uses a callback!\nThis is problematic and might cause issues in your lambda.` - : `Warning: context.done called twice within handler '${funName}'!`; - this.serverlessLog(warning); - debugLog('requestId:', requestId); + // Timeout clearing if needed + if (this._clearTimeout(requestId)) return; - return; - } + // User should not call context.done twice + if (this.requests[requestId].done) { + this.printBlankLine(); + const warning = fromPromise + ? `Warning: handler '${funName}' returned a promise and also uses a callback!\nThis is problematic and might cause issues in your lambda.` + : `Warning: context.done called twice within handler '${funName}'!`; + this.serverlessLog(warning); + debugLog('requestId:', requestId); - this.requests[requestId].done = true; - - let result = data; - let responseName = 'default'; - const responseContentType = endpoint.responseContentType; - const contentHandling = endpoint.contentHandling; - - /* RESPONSE SELECTION (among endpoint's possible responses) */ - - // Failure handling - let errorStatusCode = 0; - if (err) { - // Since the --useSeparateProcesses option loads the handler in - // a separate process and serverless-offline communicates with it - // over IPC, we are unable to catch JavaScript unhandledException errors - // when the handler code contains bad JavaScript. Instead, we "catch" - // it here and reply in the same way that we would have above when - // we lazy-load the non-IPC handler function. - if (this.options.useSeparateProcesses && err.ipcException) { - return this._reply500(response, `Error while loading ${funName}`, err); - } + return; + } - const errorMessage = (err.message || err).toString(); + this.requests[requestId].done = true; + + let result = data; + let responseName = 'default'; + const responseContentType = endpoint.responseContentType; + const contentHandling = endpoint.contentHandling; + + /* RESPONSE SELECTION (among endpoint's possible responses) */ + + // Failure handling + let errorStatusCode = 0; + if (err) { + // Since the --useSeparateProcesses option loads the handler in + // a separate process and serverless-offline communicates with it + // over IPC, we are unable to catch JavaScript unhandledException errors + // when the handler code contains bad JavaScript. Instead, we "catch" + // it here and reply in the same way that we would have above when + // we lazy-load the non-IPC handler function. + if (this.options.useSeparateProcesses && err.ipcException) { + return this._reply500(response, `Error while loading ${funName}`, err, requestId); + } - const re = /\[(\d{3})]/; - const found = errorMessage.match(re); - if (found && found.length > 1) { - errorStatusCode = found[1]; - } - else { - errorStatusCode = '500'; - } + const errorMessage = (err.message || err).toString(); - // Mocks Lambda errors - result = { - errorMessage, - errorType: err.constructor.name, - stackTrace: this._getArrayStackTrace(err.stack), - }; + const re = /\[(\d{3})]/; + const found = errorMessage.match(re); + if (found && found.length > 1) { + errorStatusCode = found[1]; + } + else { + errorStatusCode = '500'; + } - this.serverlessLog(`Failure: ${errorMessage}`); + // Mocks Lambda errors + result = { + errorMessage, + errorType: err.constructor.name, + stackTrace: this._getArrayStackTrace(err.stack), + }; - if (result.stackTrace) { - debugLog(result.stackTrace.join('\n ')); - } + this.serverlessLog(`Failure: ${errorMessage}`); - for (const key in endpoint.responses) { - if (key !== 'default' && errorMessage.match(`^${endpoint.responses[key].selectionPattern || key}$`)) { - responseName = key; - break; - } - } - } + if (result.stackTrace) { + debugLog(result.stackTrace.join('\n ')); + } - debugLog(`Using response '${responseName}'`); - const chosenResponse = endpoint.responses[responseName]; + for (const key in endpoint.responses) { + if (key !== 'default' && errorMessage.match(`^${endpoint.responses[key].selectionPattern || key}$`)) { + responseName = key; + break; + } + } + } - /* RESPONSE PARAMETERS PROCCESSING */ + debugLog(`Using response '${responseName}'`); + const chosenResponse = endpoint.responses[responseName]; - const responseParameters = chosenResponse.responseParameters; + /* RESPONSE PARAMETERS PROCCESSING */ - if (responseParameters) { + const responseParameters = chosenResponse.responseParameters; - const responseParametersKeys = Object.keys(responseParameters); + if (responseParameters) { - debugLog('_____ RESPONSE PARAMETERS PROCCESSING _____'); - debugLog(`Found ${responseParametersKeys.length} responseParameters for '${responseName}' response`); + const responseParametersKeys = Object.keys(responseParameters); - responseParametersKeys.forEach(key => { + debugLog('_____ RESPONSE PARAMETERS PROCCESSING _____'); + debugLog(`Found ${responseParametersKeys.length} responseParameters for '${responseName}' response`); - // responseParameters use the following shape: "key": "value" - const value = responseParameters[key]; - const keyArray = key.split('.'); // eg: "method.response.header.location" - const valueArray = value.split('.'); // eg: "integration.response.body.redirect.url" + responseParametersKeys.forEach(key => { - debugLog(`Processing responseParameter "${key}": "${value}"`); + // responseParameters use the following shape: "key": "value" + const value = responseParameters[key]; + const keyArray = key.split('.'); // eg: "method.response.header.location" + const valueArray = value.split('.'); // eg: "integration.response.body.redirect.url" - // For now the plugin only supports modifying headers - if (key.startsWith('method.response.header') && keyArray[3]) { + debugLog(`Processing responseParameter "${key}": "${value}"`); - const headerName = keyArray.slice(3).join('.'); - let headerValue; - debugLog('Found header in left-hand:', headerName); + // For now the plugin only supports modifying headers + if (key.startsWith('method.response.header') && keyArray[3]) { - if (value.startsWith('integration.response')) { - if (valueArray[2] === 'body') { + const headerName = keyArray.slice(3).join('.'); + let headerValue; + debugLog('Found header in left-hand:', headerName); - debugLog('Found body in right-hand'); - headerValue = (valueArray[3] ? jsonPath(result, valueArray.slice(3).join('.')) : result).toString(); + if (value.startsWith('integration.response')) { + if (valueArray[2] === 'body') { - } - else { - this.printBlankLine(); - this.serverlessLog(`Warning: while processing responseParameter "${key}": "${value}"`); - this.serverlessLog(`Offline plugin only supports "integration.response.body[.JSON_path]" right-hand responseParameter. Found "${value}" instead. Skipping.`); - this.logPluginIssue(); - this.printBlankLine(); - } - } - else { - headerValue = value.match(/^'.*'$/) ? value.slice(1, -1) : value; // See #34 - } - // Applies the header; - debugLog(`Will assign "${headerValue}" to header "${headerName}"`); - response.header(headerName, headerValue); + debugLog('Found body in right-hand'); + headerValue = (valueArray[3] ? jsonPath(result, valueArray.slice(3).join('.')) : result).toString(); } else { this.printBlankLine(); this.serverlessLog(`Warning: while processing responseParameter "${key}": "${value}"`); - this.serverlessLog(`Offline plugin only supports "method.response.header.PARAM_NAME" left-hand responseParameter. Found "${key}" instead. Skipping.`); + this.serverlessLog(`Offline plugin only supports "integration.response.body[.JSON_path]" right-hand responseParameter. Found "${value}" instead. Skipping.`); this.logPluginIssue(); this.printBlankLine(); } - }); + } + else { + headerValue = value.match(/^'.*'$/) ? value.slice(1, -1) : value; // See #34 + } + // Applies the header; + debugLog(`Will assign "${headerValue}" to header "${headerName}"`); + response.header(headerName, headerValue); + + } + else { + this.printBlankLine(); + this.serverlessLog(`Warning: while processing responseParameter "${key}": "${value}"`); + this.serverlessLog(`Offline plugin only supports "method.response.header.PARAM_NAME" left-hand responseParameter. Found "${key}" instead. Skipping.`); + this.logPluginIssue(); + this.printBlankLine(); } + }); + } - let statusCode = 200; + let statusCode = 200; - if (integration === 'lambda') { + if (integration === 'lambda') { - const endpointResponseHeaders = (endpoint.response && endpoint.response.headers) || {}; + const endpointResponseHeaders = (endpoint.response && endpoint.response.headers) || {}; - Object.keys(endpointResponseHeaders) - .filter(key => typeof endpointResponseHeaders[key] === 'string' && /^'.*?'$/.test(endpointResponseHeaders[key])) - .forEach(key => response.header(key, endpointResponseHeaders[key].slice(1, endpointResponseHeaders[key].length - 1))); + Object.keys(endpointResponseHeaders) + .filter(key => typeof endpointResponseHeaders[key] === 'string' && /^'.*?'$/.test(endpointResponseHeaders[key])) + .forEach(key => response.header(key, endpointResponseHeaders[key].slice(1, endpointResponseHeaders[key].length - 1))); - /* LAMBDA INTEGRATION RESPONSE TEMPLATE PROCCESSING */ + /* LAMBDA INTEGRATION RESPONSE TEMPLATE PROCCESSING */ - // If there is a responseTemplate, we apply it to the result - const responseTemplates = chosenResponse.responseTemplates; + // If there is a responseTemplate, we apply it to the result + const responseTemplates = chosenResponse.responseTemplates; - if (typeof responseTemplates === 'object') { - const responseTemplatesKeys = Object.keys(responseTemplates); + if (typeof responseTemplates === 'object') { + const responseTemplatesKeys = Object.keys(responseTemplates); - if (responseTemplatesKeys.length) { + if (responseTemplatesKeys.length) { - // BAD IMPLEMENTATION: first key in responseTemplates - const responseTemplate = responseTemplates[responseContentType]; + // BAD IMPLEMENTATION: first key in responseTemplates + const responseTemplate = responseTemplates[responseContentType]; - if (responseTemplate && responseTemplate !== '\n') { + if (responseTemplate && responseTemplate !== '\n') { - debugLog('_____ RESPONSE TEMPLATE PROCCESSING _____'); - debugLog(`Using responseTemplate '${responseContentType}'`); + debugLog('_____ RESPONSE TEMPLATE PROCCESSING _____'); + debugLog(`Using responseTemplate '${responseContentType}'`); - try { - const reponseContext = createVelocityContext(request, this.velocityContextOptions, result); - result = renderVelocityTemplateObject({ root: responseTemplate }, reponseContext).root; - } - catch (error) { - this.serverlessLog(`Error while parsing responseTemplate '${responseContentType}' for lambda ${funName}:`); - console.log(error.stack); - } - } + try { + const reponseContext = createVelocityContext(request, this.velocityContextOptions, result); + result = renderVelocityTemplateObject({ root: responseTemplate }, reponseContext).root; + } + catch (error) { + this.serverlessLog(`Error while parsing responseTemplate '${responseContentType}' for lambda ${funName}:`); + console.log(error.stack); } } + } + } - /* LAMBDA INTEGRATION HAPIJS RESPONSE CONFIGURATION */ + /* LAMBDA INTEGRATION HAPIJS RESPONSE CONFIGURATION */ - statusCode = errorStatusCode !== 0 ? errorStatusCode : (chosenResponse.statusCode || 200); + statusCode = errorStatusCode !== 0 ? errorStatusCode : (chosenResponse.statusCode || 200); - if (!chosenResponse.statusCode) { - this.printBlankLine(); - this.serverlessLog(`Warning: No statusCode found for response "${responseName}".`); - } + if (!chosenResponse.statusCode) { + this.printBlankLine(); + this.serverlessLog(`Warning: No statusCode found for response "${responseName}".`); + } - response.header('Content-Type', responseContentType, { - override: false, // Maybe a responseParameter set it already. See #34 - }); + response.header('Content-Type', responseContentType, { + override: false, // Maybe a responseParameter set it already. See #34 + }); - response.statusCode = statusCode; + response.statusCode = statusCode; - if (contentHandling === 'CONVERT_TO_BINARY') { - response.encoding = 'binary'; - response.source = Buffer.from(result, 'base64'); - response.variety = 'buffer'; - } - else { - if (result && result.body && typeof result.body !== 'string') { - return this._reply500(response, 'According to the API Gateway specs, the body content must be stringified. Check your Lambda response and make sure you are invoking JSON.stringify(YOUR_CONTENT) on your body object', {}); - } - response.source = result; - } + if (contentHandling === 'CONVERT_TO_BINARY') { + response.encoding = 'binary'; + response.source = Buffer.from(result, 'base64'); + response.variety = 'buffer'; + } + else { + if (result && result.body && typeof result.body !== 'string') { + return this._reply500(response, 'According to the API Gateway specs, the body content must be stringified. Check your Lambda response and make sure you are invoking JSON.stringify(YOUR_CONTENT) on your body object', {}, requestId); } - else if (integration === 'lambda-proxy') { + response.source = result; + } + } + else if (integration === 'lambda-proxy') { - /* LAMBDA PROXY INTEGRATION HAPIJS RESPONSE CONFIGURATION */ + /* LAMBDA PROXY INTEGRATION HAPIJS RESPONSE CONFIGURATION */ - response.statusCode = statusCode = result.statusCode || 200; + response.statusCode = statusCode = result.statusCode || 200; - const headers = {}; - if (result.headers) { - Object.keys(result.headers).forEach(header => { - headers[header] = (headers[header] || []).concat(result.headers[header]); - }); - } - if (result.multiValueHeaders) { - Object.keys(result.multiValueHeaders).forEach(header => { - headers[header] = (headers[header] || []).concat(result.multiValueHeaders[header]); - }); - } + const headers = {}; + if (result.headers) { + Object.keys(result.headers).forEach(header => { + headers[header] = (headers[header] || []).concat(result.headers[header]); + }); + } + if (result.multiValueHeaders) { + Object.keys(result.multiValueHeaders).forEach(header => { + headers[header] = (headers[header] || []).concat(result.multiValueHeaders[header]); + }); + } - debugLog('headers', headers); + debugLog('headers', headers); - Object.keys(headers).forEach(header => { - if (header.toLowerCase() === 'set-cookie') { - headers[header].forEach(headerValue => { - const cookieName = headerValue.slice(0, headerValue.indexOf('=')); - const cookieValue = headerValue.slice(headerValue.indexOf('=') + 1); - reply.state(cookieName, cookieValue, { encoding: 'none', strictHeader: false }); - }); - } - else { - headers[header].forEach(headerValue => { - // it looks like Hapi doesn't support multiple headers with the same name, - // appending values is the closest we can come to the AWS behavior. - response.header(header, headerValue, { append: true }); - }); - } + Object.keys(headers).forEach(header => { + if (header.toLowerCase() === 'set-cookie') { + headers[header].forEach(headerValue => { + const cookieName = headerValue.slice(0, headerValue.indexOf('=')); + const cookieValue = headerValue.slice(headerValue.indexOf('=') + 1); + reply.state(cookieName, cookieValue, { encoding: 'none', strictHeader: false }); }); + } + else { + headers[header].forEach(headerValue => { + // it looks like Hapi doesn't support multiple headers with the same name, + // appending values is the closest we can come to the AWS behavior. + response.header(header, headerValue, { append: true }); + }); + } + }); - response.header('Content-Type', 'application/json', { override: false, duplicate: false }); + response.header('Content-Type', 'application/json', { override: false, duplicate: false }); - if (typeof result.body !== 'undefined') { - if (result.isBase64Encoded) { - response.encoding = 'binary'; - response.source = Buffer.from(result.body, 'base64'); - response.variety = 'buffer'; - } - else { - if (result.body && typeof result.body !== 'string') { - return this._reply500(response, 'According to the API Gateway specs, the body content must be stringified. Check your Lambda response and make sure you are invoking JSON.stringify(YOUR_CONTENT) on your body object', {}); - } - response.source = result.body; - } + if (typeof result.body !== 'undefined') { + if (result.isBase64Encoded) { + response.encoding = 'binary'; + response.source = Buffer.from(result.body, 'base64'); + response.variety = 'buffer'; + } + else { + if (result.body && typeof result.body !== 'string') { + return this._reply500(response, 'According to the API Gateway specs, the body content must be stringified. Check your Lambda response and make sure you are invoking JSON.stringify(YOUR_CONTENT) on your body object', {}, requestId); } + response.source = result.body; } + } + } - // Log response - let whatToLog = result; + // Log response + let whatToLog = result; - try { - whatToLog = JSON.stringify(result); - } - catch (error) { - // nothing - } - finally { - if (this.options.printOutput) this.serverlessLog(err ? `Replying ${statusCode}` : `[${statusCode}] ${whatToLog}`); - debugLog('requestId:', requestId); - } + try { + whatToLog = JSON.stringify(result); + } + catch (error) { + // nothing + } + finally { + if (this.options.printOutput) this.serverlessLog(err ? `Replying ${statusCode}` : `[${statusCode}] ${whatToLog}`); + debugLog('requestId:', requestId); + } - // Bon voyage! - response.send(); - }); + // Bon voyage! + response.send(); + }); - // Now we are outside of createLambdaContext, so this happens before the handler gets called: + // Now we are outside of createLambdaContext, so this happens before the handler gets called: - // We cannot use Hapijs's timeout feature because the logic above can take a significant time, so we implement it ourselves - this.requests[requestId].timeout = this.options.noTimeout ? null : setTimeout( - this._replyTimeout.bind(this, response, funName, funOptions.funTimeout, requestId), - funOptions.funTimeout - ); + // We cannot use Hapijs's timeout feature because the logic above can take a significant time, so we implement it ourselves + this.requests[requestId].timeout = this.options.noTimeout ? null : setTimeout( + this._replyTimeout.bind(this, response, funName, funOptions.funTimeout, requestId), + funOptions.funTimeout + ); - // If request body validation is enabled, validate body against the request model. - if (requestBodyValidationModel && !this.options.disableModelValidation) { - try { - requestBodyValidator.validate(requestBodyValidationModel, event.body); - } - catch (error) { - // When request body validation fails, APIG will return back 400 as detailed in: - // https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-method-request-validation.html - return this._replyError(400, response, `Invalid request body for '${funName}' handler`, error); - } - } + // If request body validation is enabled, validate body against the request model. + if (requestBodyValidationModel && !this.options.disableModelValidation) { + try { + requestBodyValidator.validate(requestBodyValidationModel, event.body); + } + catch (error) { + // When request body validation fails, APIG will return back 400 as detailed in: + // https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-method-request-validation.html + return this._replyError(400, response, `Invalid request body for '${funName}' handler`, error, requestId); + } + } - // Finally we call the handler - debugLog('_____ CALLING HANDLER _____'); - try { - const x = handler(event, lambdaContext, lambdaContext.done); + // Finally we call the handler + debugLog('_____ CALLING HANDLER _____'); + try { + const x = handler(event, lambdaContext, lambdaContext.done); - // Promise support - if (!this.requests[requestId].done) { - if (x && typeof x.then === 'function' && typeof x.catch === 'function') x.then(lambdaContext.succeed).catch(lambdaContext.fail); - else if (x instanceof Error) lambdaContext.fail(x); - } - } - catch (error) { - return this._reply500(response, `Uncaught error in your '${funName}' handler`, error); - } - finally { - setTimeout(() => { - this._clearTimeout(requestId); - delete this.requests[requestId]; - }, 0); - } - }, - }); - }); + // Promise support + if (!this.requests[requestId].done) { + if (x && typeof x.then === 'function' && typeof x.catch === 'function') x.then(lambdaContext.succeed).catch(lambdaContext.fail); + else if (x instanceof Error) lambdaContext.fail(x); + } + } + catch (error) { + return this._reply500(response, `Uncaught error in your '${funName}' handler`, error, requestId); + } + }, }); } @@ -1058,7 +1186,12 @@ class Offline { } // Bad news - _replyError(responseCode, response, message, err) { + _replyError(responseCode, response, message, err, requestId) { + + if (this._clearTimeout(requestId)) return; + + this.requests[requestId].done = true; + const stackTrace = this._getArrayStackTrace(err.stack); this.serverlessLog(message); @@ -1084,14 +1217,16 @@ class Offline { response.send(); } - _reply500(response, message, err) { + _reply500(response, message, err, requestId) { // APIG replies 200 by default on failures - this._replyError(200, response, message, err); + this._replyError(200, response, message, err, requestId); } _replyTimeout(response, funName, funTimeout, requestId) { if (this.currentRequestId !== requestId) return; + this.requests[requestId].done = true; + this.serverlessLog(`Replying timeout after ${funTimeout}ms`); /* eslint-disable no-param-reassign */ response.statusCode = 503; @@ -1101,7 +1236,8 @@ class Offline { } _clearTimeout(requestId) { - const { timeout } = this.requests[requestId]; + const timeout = this.requests[requestId].timeout; + if (timeout && timeout._called) return true; clearTimeout(timeout); } @@ -1204,7 +1340,6 @@ class Offline { } _logAndExit() { - // eslint-disable-next-line console.log.apply(null, arguments); process.exit(0); } From e32b9fa2ab632b29879f5ab7b98846ad91503470 Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Tue, 14 May 2019 06:12:04 +0300 Subject: [PATCH 02/71] Added support for queryStringParameters --- .gitignore | 10 +- manual_test_websocket/handler.js | 6 +- manual_test_websocket/package-lock.json | 6 +- manual_test_websocket/package.json | 2 +- manual_test_websocket/test/e2e/ws.e2e.js | 26 +- package-lock.json | 490 +++++++++-------------- src/index.js | 27 +- 7 files changed, 236 insertions(+), 331 deletions(-) diff --git a/.gitignore b/.gitignore index a6c6005e8..6afa0406c 100644 --- a/.gitignore +++ b/.gitignore @@ -93,6 +93,9 @@ crashlytics.properties crashlytics-build.properties fabric.properties +# Auto generated by npm run deploy-offline or deploy-aws +manual_test_websocket/serverless.yml + ### Intellij Patch ### # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 @@ -104,6 +107,7 @@ fabric.properties # auto-generated tag files tags ======= - -.idea/ -manual_test/.serverless + +.idea/ +.serverless +.dynamodb diff --git a/manual_test_websocket/handler.js b/manual_test_websocket/handler.js index ef4171391..b6bbd5cfb 100644 --- a/manual_test_websocket/handler.js +++ b/manual_test_websocket/handler.js @@ -18,8 +18,12 @@ const errorResponse = { }; module.exports.connect = async (event, context) => { + console.log('event:'); + console.log(event); + console.log('context:'); + console.log(context); const listener=await ddb.get({TableName:'listeners', Key:{name:'default'}}).promise(); - if (listener.Item) await sendToClient(JSON.stringify({action:'update', event:'connect', info:{id:event.requestContext.connectionId}}), listener.Item.id, newAWSApiGatewayManagementApi(event, context)).catch(()=>{}); + if (listener.Item) await sendToClient(JSON.stringify({action:'update', event:'connect', info:{id:event.requestContext.connectionId, queryStringParameters:event.queryStringParameters}}), listener.Item.id, newAWSApiGatewayManagementApi(event, context)).catch(()=>{}); return successfullResponse; }; diff --git a/manual_test_websocket/package-lock.json b/manual_test_websocket/package-lock.json index acb22d0c2..b964d0dc7 100644 --- a/manual_test_websocket/package-lock.json +++ b/manual_test_websocket/package-lock.json @@ -47,9 +47,9 @@ "dev": true }, "aws-sdk": { - "version": "2.449.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.449.0.tgz", - "integrity": "sha512-ywvqLoBUlibAkud+A3eXZbGv6pBZwqb/DolYvJJR834E8Dvp8+bYZY1+gCDe9a5hp15ICb2jD+vOM2W6ljUlHw==", + "version": "2.453.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.453.0.tgz", + "integrity": "sha512-UsofQeX8XVElr4+bqw47jOZUGNiwUdFVTNyOdOHcK14RHH2ySsve6JWyvQT8L2rS//XQP1OBIQo20PAl1Zf7ig==", "requires": { "buffer": "4.9.1", "events": "1.1.1", diff --git a/manual_test_websocket/package.json b/manual_test_websocket/package.json index a74701349..9039c28e9 100644 --- a/manual_test_websocket/package.json +++ b/manual_test_websocket/package.json @@ -13,7 +13,7 @@ "author": "", "license": "MIT", "dependencies": { - "aws-sdk": "^2.449.0" + "aws-sdk": "^2.453.0" }, "devDependencies": { "chai": "^4.2.0", diff --git a/manual_test_websocket/test/e2e/ws.e2e.js b/manual_test_websocket/test/e2e/ws.e2e.js index 66314b629..51c4d883b 100644 --- a/manual_test_websocket/test/e2e/ws.e2e.js +++ b/manual_test_websocket/test/e2e/ws.e2e.js @@ -8,14 +8,16 @@ const WebSocketTester=require('../support/WebSocketTester'); describe('serverless', ()=>{ describe('with WebSocket support', ()=>{ let clients=[]; - const createWebSocket=async ()=>{ + const createWebSocket=async (qs)=>{ const ws=new WebSocketTester(); - await ws.open(endpoint); + let url=endpoint; + if (qs) url=`${endpoint}?${qs}`; + await ws.open(url); clients.push(ws); return ws; }; - const createClient=async ()=>{ - const ws=await createWebSocket(); + const createClient=async (qs)=>{ + const ws=await createWebSocket(qs); ws.send(JSON.stringify({action:'getClientInfo'})); const json=await ws.receive1(); const id=JSON.parse(json).info.id; @@ -99,16 +101,16 @@ describe('serverless', ()=>{ await ws.receive1(); const c1=await createClient(); - expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c1.id}}); + expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c1.id, queryStringParameters:{}}}); const c2=await createClient(); - expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c2.id}}); + expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c2.id, queryStringParameters:{}}}); c2.ws.close(); expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'disconnect', info:{id:c2.id}}); const c3=await createClient(); - expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c3.id}}); + expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c3.id, queryStringParameters:{}}}); c1.ws.close(); expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'disconnect', info:{id:c1.id}}); @@ -116,5 +118,15 @@ describe('serverless', ()=>{ c3.ws.close(); expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'disconnect', info:{id:c3.id}}); }).timeout(6000); + + it('should be able to parse query string', async ()=>{ + const now=''+Date.now(); + const ws=await createWebSocket(); + await ws.send(JSON.stringify({action:'registerListener'})); + await ws.receive1(); + + const c1=await createClient(`now=${now}&before=123456789`); + expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c1.id, queryStringParameters:{now, before:'123456789'}}}); + }); }); }); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 933fd5cb4..e177051a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "serverless-offline", - "version": "4.10.0", + "version": "4.9.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -13,48 +13,6 @@ "@babel/highlight": "^7.0.0" } }, - "@babel/generator": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.4.4.tgz", - "integrity": "sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ==", - "dev": true, - "requires": { - "@babel/types": "^7.4.4", - "jsesc": "^2.5.1", - "lodash": "^4.17.11", - "source-map": "^0.5.0", - "trim-right": "^1.0.1" - } - }, - "@babel/helper-function-name": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", - "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.0.0", - "@babel/template": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", - "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", - "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", - "dev": true, - "requires": { - "@babel/types": "^7.4.4" - } - }, "@babel/highlight": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", @@ -66,51 +24,6 @@ "js-tokens": "^4.0.0" } }, - "@babel/parser": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.4.tgz", - "integrity": "sha512-5pCS4mOsL+ANsFZGdvNLybx4wtqAZJ0MJjMHxvzI3bvIsz6sQvzW8XX92EYIkiPtIvcfG3Aj+Ir5VNyjnZhP7w==", - "dev": true - }, - "@babel/template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", - "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.4.4", - "@babel/types": "^7.4.4" - } - }, - "@babel/traverse": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.4.4.tgz", - "integrity": "sha512-Gw6qqkw/e6AGzlyj9KnkabJX7VcubqPtkUQVAwkc0wUMldr3A/hezNB3Rc5eIvId95iSGkGIOe5hh1kMKf951A==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/generator": "^7.4.4", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.4.4", - "@babel/parser": "^7.4.4", - "@babel/types": "^7.4.4", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.11" - } - }, - "@babel/types": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.4.4.tgz", - "integrity": "sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ==", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.11", - "to-fast-properties": "^2.0.0" - } - }, "@sinonjs/commons": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz", @@ -255,16 +168,6 @@ "sprintf-js": "~1.0.2" } }, - "aria-query": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", - "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=", - "dev": true, - "requires": { - "ast-types-flow": "0.0.7", - "commander": "^2.11.0" - } - }, "array-from": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", @@ -287,56 +190,29 @@ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true }, - "ast-types-flow": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", - "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", - "dev": true - }, "astral-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", "dev": true }, - "axobject-query": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.2.tgz", - "integrity": "sha512-MCeek8ZH7hKyO1rWUbKNQBbl4l2eY0ntk7OGi+q0RlafrCnfPxC06WZA+uebCfmYp4mNU9jRBP1AhGyf8+W3ww==", - "dev": true, - "requires": { - "ast-types-flow": "0.0.7" - } + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" }, "b64": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/b64/-/b64-3.0.3.tgz", "integrity": "sha512-Pbeh0i6OLubPJdIdCepn8ZQHwN2MWznZHbHABSTEfQ706ie+yuxNSaPdqX1xRatT6WanaS1EazMiSg0NUW2XxQ==" }, - "babel-eslint": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.0.1.tgz", - "integrity": "sha512-z7OT1iNV+TjOwHNLLyJk+HN+YVWX+CLE6fPD2SymJZOZQBs+QIexFjhm4keGTm8MW9xr4EC9Q0PbaLB24V5GoQ==", - "dev": true, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.0.0", - "@babel/traverse": "^7.0.0", - "@babel/types": "^7.0.0", - "eslint-scope": "3.7.1", - "eslint-visitor-keys": "^1.0.0" - }, - "dependencies": { - "eslint-scope": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", - "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - } + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" } }, "balanced-match": { @@ -345,6 +221,11 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "bignumber.js": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-6.0.0.tgz", + "integrity": "sha512-x247jIuy60/+FtMRvscqfxtVHQf8AGx2hm9c6btkgC0x/hp9yt+teISNhvF8WlwRkCc5yF2fDECH8SIMe8j+GA==" + }, "boom": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/boom/-/boom-7.3.0.tgz", @@ -455,6 +336,22 @@ } } }, + "cbor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-4.0.0.tgz", + "integrity": "sha512-SWtnNIThYI4bM1cg/5AKj2oKDsrFOmQb5W4pr6jaIlbsOfl/aLHJADx9hVkAqUX4PR3iDZLp8f9S6QPP9VPXxg==", + "requires": { + "bignumber.js": "^6.0", + "commander": "^2.14.1", + "json-text-sequence": "^0.1", + "nofilter": "^0.0.3" + } + }, + "cbor-js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cbor-js/-/cbor-js-0.1.0.tgz", + "integrity": "sha1-yAzmEg84fo+qdDcN/aIdlluPx/k=" + }, "chai": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", @@ -542,8 +439,7 @@ "commander": { "version": "2.20.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", - "dev": true + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" }, "concat-map": { "version": "0.0.1", @@ -580,6 +476,11 @@ } } }, + "core-js": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", + "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==" + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -601,12 +502,6 @@ "boom": "7.x.x" } }, - "damerau-levenshtein": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz", - "integrity": "sha1-AxkcQyy27qFou3fzpV/9zLiXhRQ=", - "dev": true - }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -646,6 +541,11 @@ "object-keys": "^1.0.12" } }, + "delimit-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/delimit-stream/-/delimit-stream-0.1.0.tgz", + "integrity": "sha1-m4MZR3wOX4rrPONXrjBfwl6hzSs=" + }, "diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", @@ -681,6 +581,18 @@ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", "dev": true }, + "encodr": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/encodr/-/encodr-1.0.7.tgz", + "integrity": "sha512-3GtpSnmDeNDOMcvcjV+rurzQBy+y802omzQn46iex3mJ0NDsHKw1ay2kOdIqGYTsohwE1nH6FJCz8IXzpZZOlQ==", + "requires": { + "babel-runtime": "6.26.0", + "cbor": "4.0.0", + "cbor-js": "0.1.0", + "msgpack-lite": "0.1.26", + "utf8": "3.0.0" + } + }, "end-of-stream": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", @@ -774,39 +686,24 @@ "text-table": "^0.2.0" } }, - "eslint-config-airbnb": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-17.1.0.tgz", - "integrity": "sha512-R9jw28hFfEQnpPau01NO5K/JWMGLi6aymiF6RsnMURjTk+MqZKllCqGK/0tOvHkPi/NWSSOU2Ced/GX++YxLnw==", - "dev": true, - "requires": { - "eslint-config-airbnb-base": "^13.1.0", - "object.assign": "^4.1.0", - "object.entries": "^1.0.4" - } - }, - "eslint-config-airbnb-base": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-13.1.0.tgz", - "integrity": "sha512-XWwQtf3U3zIoKO1BbHh6aUhJZQweOwSt4c2JrPDg9FP3Ltv3+YfEv7jIDB8275tVnO/qOHbfuYg3kzw6Je7uWw==", - "dev": true, - "requires": { - "eslint-restricted-globals": "^0.1.1", - "object.assign": "^4.1.0", - "object.entries": "^1.0.4" - } - }, - "eslint-config-dherault": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/eslint-config-dherault/-/eslint-config-dherault-1.0.2.tgz", - "integrity": "sha512-Xn6eN2i0MBJF2b2k116h7LRqz4VR44vJ8N/14xRCnj0NOjAHtLeMe3ZAe+931HSMA6dNLTnd4pESKp+DGUJmgA==", + "eslint-config-nelson": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eslint-config-nelson/-/eslint-config-nelson-0.2.0.tgz", + "integrity": "sha1-rIwAVZYu+WodB5tWywMvPVB2+4Q=", "dev": true, "requires": { - "babel-eslint": "^10.0.1", - "eslint-config-airbnb": "^17.1.0", - "eslint-plugin-import": "^2.17.2", - "eslint-plugin-jsx-a11y": "^6.2.1", - "eslint-plugin-react": "^7.13.0" + "eslint-config-airbnb-base": "^11.0.0" + }, + "dependencies": { + "eslint-config-airbnb-base": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-11.3.2.tgz", + "integrity": "sha512-/fhjt/VqzBA2SRsx7ErDtv6Ayf+XLw9LIOqmpBuHFCVwyJo2EtzGWMB9fYRFBoWWQLxmNmCpenNiH0RxyeS41w==", + "dev": true, + "requires": { + "eslint-restricted-globals": "^0.1.1" + } + } } }, "eslint-import-resolver-node": { @@ -909,48 +806,6 @@ } } }, - "eslint-plugin-jsx-a11y": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.1.tgz", - "integrity": "sha512-cjN2ObWrRz0TTw7vEcGQrx+YltMvZoOEx4hWU8eEERDnBIU00OTq7Vr+jA7DFKxiwLNv4tTh5Pq2GUNEa8b6+w==", - "dev": true, - "requires": { - "aria-query": "^3.0.0", - "array-includes": "^3.0.3", - "ast-types-flow": "^0.0.7", - "axobject-query": "^2.0.2", - "damerau-levenshtein": "^1.0.4", - "emoji-regex": "^7.0.2", - "has": "^1.0.3", - "jsx-ast-utils": "^2.0.1" - } - }, - "eslint-plugin-react": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.13.0.tgz", - "integrity": "sha512-uA5LrHylu8lW/eAH3bEQe9YdzpPaFd9yAJTwTi/i/BKTD7j6aQMKVAdGM/ML72zD6womuSK7EiGtMKuK06lWjQ==", - "dev": true, - "requires": { - "array-includes": "^3.0.3", - "doctrine": "^2.1.0", - "has": "^1.0.3", - "jsx-ast-utils": "^2.1.0", - "object.fromentries": "^2.0.0", - "prop-types": "^15.7.2", - "resolve": "^1.10.1" - }, - "dependencies": { - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - } - } - }, "eslint-restricted-globals": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz", @@ -1026,6 +881,16 @@ "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", "dev": true }, + "event-lite": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.2.tgz", + "integrity": "sha512-HnSYx1BsJ87/p6swwzv+2v6B4X+uxUteoDfRxsAb1S1BePzQqOLevVmkdA15GHJVd9A9Ok6wygUR18Hu0YeV9g==" + }, + "eventemitter3": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.0.1.tgz", + "integrity": "sha512-QOCPu979MMWX9XNlfRZoin+Wm+bK1SP7vv3NGUniYwuSJK/+cPA10blMaeRgzg31RvoSFk6FsCDVa4vNryBTGA==" + }, "execa": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", @@ -1292,6 +1157,41 @@ "resolved": "https://registry.npmjs.org/hapi-cors-headers/-/hapi-cors-headers-1.0.3.tgz", "integrity": "sha512-U/y+kpVLUJ0y86fEk8yleou9C1T5wFopcWQjuxKdMXzCcymTjfSqGz59waqvngUs1SbeXav/y8Ga9C0G0L1MGg==" }, + "hapi-plugin-websocket": { + "version": "1.2.19", + "resolved": "https://registry.npmjs.org/hapi-plugin-websocket/-/hapi-plugin-websocket-1.2.19.tgz", + "integrity": "sha512-I1ExBMXlRnlyKEaUCKOdt8e7jpvYrQ/cP5laX9c9nO/yMTPeL91ZSUi8RVkKFVVjqwJcAgyOpXXptcZv9ugMNw==", + "requires": { + "boom": "7.1.1", + "hoek": "5.0.2", + "urijs": "1.19.1", + "websocket-framed": "1.0.14", + "ws": "5.1.0" + }, + "dependencies": { + "boom": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-7.1.1.tgz", + "integrity": "sha512-qwEARHTliqgEQiVkzKkkbLt3q0vRPIW60VRZ8zRnbjsm7INkPe9NxfAYDDYLZOdhxyUHa1gIe639Cx7t6RH/4A==", + "requires": { + "hoek": "5.x.x" + } + }, + "hoek": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.2.tgz", + "integrity": "sha512-NA10UYP9ufCtY2qYGkZktcQXwVyYK4zK0gkaFSB96xhtlo6V8tKXdQgx8eHolQTRemaW0uLn8BhjhwqrOU+QLQ==" + }, + "ws": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.1.0.tgz", + "integrity": "sha512-7KU/qkUXtJW9aa5WRKlo0puE1ejEoAgDb0D/Pt+lWpTkKF7Kp+MqFOtwNFwnuiYeeDpFjp0qyMniE84OjKIEqQ==", + "requires": { + "async-limiter": "~1.0.0" + } + } + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -1364,6 +1264,11 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -1440,6 +1345,11 @@ } } }, + "int64-buffer": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/int64-buffer/-/int64-buffer-0.1.10.tgz", + "integrity": "sha1-J3siiofZWtd30HwTgyAiQGpHNCM=" + }, "invert-kv": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", @@ -1542,8 +1452,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isemail": { "version": "2.2.1", @@ -1600,12 +1509,6 @@ "esprima": "^4.0.0" } }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true - }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -1618,6 +1521,14 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, + "json-text-sequence": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/json-text-sequence/-/json-text-sequence-0.1.1.tgz", + "integrity": "sha1-py8hfcSvxGKf/1/rME3BvVGi89I=", + "requires": { + "delimit-stream": "0.1.0" + } + }, "jsonpath-plus": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-0.16.0.tgz", @@ -1645,15 +1556,6 @@ "semver": "^5.6.0" } }, - "jsx-ast-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.1.0.tgz", - "integrity": "sha512-yDGDG2DS4JcqhA6blsuYbtsT09xL8AoLuUR2Gb5exrw7UEM19sBcOTq+YBBhrNbl0PUC4R4LnFu+dHg2HKeVvA==", - "dev": true, - "requires": { - "array-includes": "^3.0.3" - } - }, "just-extend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", @@ -1776,15 +1678,6 @@ "integrity": "sha512-UHuOBZ5jjsKuzbB/gRNNW8Vg8f00Emgskdq2kvZxgBJCS0aqquAuXai/SkWORlKeZEiNQWZjFZOqIUcH9LqKCw==", "dev": true }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, "map-age-cleaner": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", @@ -1963,6 +1856,17 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" }, + "msgpack-lite": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/msgpack-lite/-/msgpack-lite-0.1.26.tgz", + "integrity": "sha1-3TxQsm8FnyXn7e42REGDWOKprYk=", + "requires": { + "event-lite": "^0.1.1", + "ieee754": "^1.1.8", + "int64-buffer": "^0.1.9", + "isarray": "^1.0.0" + } + }, "mute-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", @@ -2028,6 +1932,11 @@ "semver": "^5.7.0" } }, + "nofilter": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-0.0.3.tgz", + "integrity": "sha1-JB40IHgXeoaTowQ+g/N1Z+J2QQw=" + }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -2055,12 +1964,6 @@ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "dev": true }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -2079,30 +1982,6 @@ "object-keys": "^1.0.11" } }, - "object.entries": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.0.tgz", - "integrity": "sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.12.0", - "function-bind": "^1.1.1", - "has": "^1.0.3" - } - }, - "object.fromentries": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.0.tgz", - "integrity": "sha512-9iLiI6H083uiqUuvzyY6qrlmc/Gz8hLQFOcb/Ri/0xXFkSNS3ctV+CbE6yM2+AnkYfOB3dGjdzC0wrMLIhQICA==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.11.0", - "function-bind": "^1.1.1", - "has": "^1.0.1" - } - }, "object.getownpropertydescriptors": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", @@ -2355,17 +2234,6 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, - "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "dev": true, - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" - } - }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -2381,12 +2249,6 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, - "react-is": { - "version": "16.8.6", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", - "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==", - "dev": true - }, "read-pkg": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", @@ -2408,6 +2270,11 @@ "read-pkg": "^2.0.0" } }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + }, "regexpp": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", @@ -2578,12 +2445,6 @@ } } }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - }, "spdx-correct": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", @@ -2812,12 +2673,6 @@ "os-tmpdir": "~1.0.2" } }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - }, "topo": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/topo/-/topo-2.0.2.tgz", @@ -2838,12 +2693,6 @@ "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=" }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", - "dev": true - }, "tslib": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", @@ -2874,6 +2723,16 @@ "punycode": "^2.1.0" } }, + "urijs": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.1.tgz", + "integrity": "sha512-xVrGVi94ueCJNrBSTjWqjvtgvl3cyOTThp2zaMaFNGp3F542TR6sM3f2o8RqZl+AwteClSVmoCyt0ka4RjQOQg==" + }, + "utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", + "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==" + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -2904,6 +2763,15 @@ } } }, + "websocket-framed": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/websocket-framed/-/websocket-framed-1.0.14.tgz", + "integrity": "sha512-gx/t6+hrWc9TASmetg6Q7G2MeOFqG910xVmhtGutInsdY3OdghdyzOOjQcUsfzbqQ6ZxERvjS67ooxPFKx8YCg==", + "requires": { + "encodr": "1.0.7", + "eventemitter3": "3.0.1" + } + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -3020,6 +2888,14 @@ "mkdirp": "^0.5.1" } }, + "ws": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", + "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", + "requires": { + "async-limiter": "~1.0.0" + } + }, "y18n": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", diff --git a/src/index.js b/src/index.js index 299e1de6e..d42a869a3 100644 --- a/src/index.js +++ b/src/index.js @@ -460,15 +460,24 @@ class Offline { only: true, initially: true, // subprotocol: "quux/1.0", - connect: ({ ctx, ws }) => { - // ctx.to = setInterval(() => { - // ws.send(JSON.stringify({ cmd: "PING" })) - // }, 5000); - const connectionId=utils.randomId(); - console.log('connect '+connectionId); - this.clients.set(ws, connectionId); - - doAction(ws, connectionId, '$connect', {requestContext:{eventType:'CONNECT', connectionId}}); + connect: ({ ws, req }) => { + const parseQuery=(queryString)=>{ + const query = {}; const parts=req.url.split('?'); + if (2>parts.length) return {}; + var pairs = parts[1].split('&'); + pairs.forEach((pair)=>{ + const kv = pair.split('='); + query[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1] || ''); + }); + return query; + }; + + const queryStringParameters = parseQuery(req.url); + const connectionId=utils.randomId(); + console.log('connect '+connectionId); + this.clients.set(ws, connectionId); + + doAction(ws, connectionId, '$connect', {requestContext:{eventType:'CONNECT', connectionId}, queryStringParameters}); }, disconnect: ({ ctx, ws }) => { // if (ctx.to !== null) { From 1ea68e0b22fc9f002d40be347aead9243d4f87fb Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Tue, 21 May 2019 16:04:32 +0300 Subject: [PATCH 03/71] Bug fixes * Fix bug of test stop running when sending a an update to a non existing connection. * Fix but when querry string support was not equally handled as in AWS. --- .gitignore | 3 - manual_test_websocket/handler.js | 18 +- manual_test_websocket/package-lock.json | 219 +++++++++++++++++- manual_test_websocket/package.json | 3 +- manual_test_websocket/scripts/serverless..yml | 6 + manual_test_websocket/test/e2e/ws.e2e.js | 24 +- src/index.js | 4 +- 7 files changed, 257 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 6afa0406c..56074c756 100644 --- a/.gitignore +++ b/.gitignore @@ -93,9 +93,6 @@ crashlytics.properties crashlytics-build.properties fabric.properties -# Auto generated by npm run deploy-offline or deploy-aws -manual_test_websocket/serverless.yml - ### Intellij Patch ### # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 diff --git a/manual_test_websocket/handler.js b/manual_test_websocket/handler.js index b6bbd5cfb..c61952d95 100644 --- a/manual_test_websocket/handler.js +++ b/manual_test_websocket/handler.js @@ -17,13 +17,21 @@ const errorResponse = { body: 'Request is not OK.' }; +// module.exports.http = async (event, context) => { +// return successfullResponse; +// }; + module.exports.connect = async (event, context) => { - console.log('event:'); - console.log(event); - console.log('context:'); - console.log(context); const listener=await ddb.get({TableName:'listeners', Key:{name:'default'}}).promise(); - if (listener.Item) await sendToClient(JSON.stringify({action:'update', event:'connect', info:{id:event.requestContext.connectionId, queryStringParameters:event.queryStringParameters}}), listener.Item.id, newAWSApiGatewayManagementApi(event, context)).catch(()=>{}); + + if (listener.Item) { + const timeout=new Promise((resolve) => setTimeout(resolve,100)); + const send=sendToClient( // sendToClient won't return on AWS when client doesn't exits so we set a timeout + JSON.stringify({action:'update', event:'connect', info:{id:event.requestContext.connectionId, queryStringParameters:event.queryStringParameters}}), + listener.Item.id, + newAWSApiGatewayManagementApi(event, context)).catch(()=>{}); + await Promise.race([send, timeout]); + } return successfullResponse; }; diff --git a/manual_test_websocket/package-lock.json b/manual_test_websocket/package-lock.json index b964d0dc7..adae25f87 100644 --- a/manual_test_websocket/package-lock.json +++ b/manual_test_websocket/package-lock.json @@ -4,6 +4,34 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@types/chai": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz", + "integrity": "sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==", + "dev": true + }, + "@types/cookiejar": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.1.tgz", + "integrity": "sha512-aRnpPa7ysx3aNW60hTiCtLHlQaIFsXFCgQlpakNgDNVFzbtusSY8PwjAQgRWfSk0ekNoBjO51eQRB6upA9uuyw==", + "dev": true + }, + "@types/node": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz", + "integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==", + "dev": true + }, + "@types/superagent": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-3.8.7.tgz", + "integrity": "sha512-9KhCkyXv268A2nZ1Wvu7rQWM+BmdYUVkycFeNnYrUL5Zwu7o8wPQ3wBfW59dDP+wuoxw0ww8YKgTNv8j/cgscA==", + "dev": true, + "requires": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, "ansi-colors": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", @@ -46,10 +74,16 @@ "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", "dev": true }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, "aws-sdk": { - "version": "2.453.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.453.0.tgz", - "integrity": "sha512-UsofQeX8XVElr4+bqw47jOZUGNiwUdFVTNyOdOHcK14RHH2ySsve6JWyvQT8L2rS//XQP1OBIQo20PAl1Zf7ig==", + "version": "2.459.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.459.0.tgz", + "integrity": "sha512-b1ziocvzv0NkV0oh5QRpGl28iYKfPqgcK52zvM02/sCmDH4YGuThC6ckFh/sKpXBYK7ToCg1St0LmtBK6siFbg==", "requires": { "buffer": "4.9.1", "events": "1.1.1", @@ -134,6 +168,21 @@ "type-detect": "^4.0.5" } }, + "chai-http": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/chai-http/-/chai-http-4.3.0.tgz", + "integrity": "sha512-zFTxlN7HLMv+7+SPXZdkd5wUlK+KxH6Q7bIEMiEx0FK3zuuMqL7cwICAQ0V1+yYRozBburYuxN1qZstgHpFZQg==", + "dev": true, + "requires": { + "@types/chai": "4", + "@types/superagent": "^3.8.3", + "cookiejar": "^2.1.1", + "is-ip": "^2.0.0", + "methods": "^1.1.2", + "qs": "^6.5.1", + "superagent": "^3.7.0" + } + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -194,12 +243,39 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -246,6 +322,12 @@ "object-keys": "^1.0.12" } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, "diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", @@ -336,6 +418,12 @@ "strip-eof": "^1.0.0" } }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, "find-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", @@ -354,6 +442,23 @@ "is-buffer": "~2.0.3" } }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", + "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==", + "dev": true + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -479,6 +584,12 @@ "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", "dev": true }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true + }, "is": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/is/-/is-0.2.7.tgz", @@ -509,6 +620,15 @@ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "dev": true }, + "is-ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-2.0.0.tgz", + "integrity": "sha1-aO6gfooKCpTC0IDdZ0xzGrKkYas=", + "dev": true, + "requires": { + "ip-regex": "^2.0.0" + } + }, "is-regex": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", @@ -613,6 +733,33 @@ "p-is-promise": "^2.0.0" } }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", + "dev": true + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "dev": true, + "requires": { + "mime-db": "1.40.0" + } + }, "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -852,6 +999,12 @@ "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", "dev": true }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, "progress": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", @@ -873,11 +1026,32 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + }, "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -908,6 +1082,12 @@ "node.flow": "1.2.3" } }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, "sax": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", @@ -974,6 +1154,15 @@ "strip-ansi": "^4.0.0" } }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", @@ -995,6 +1184,24 @@ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true }, + "superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "dev": true, + "requires": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.2.0", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.3.5" + } + }, "supports-color": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", @@ -1030,6 +1237,12 @@ "querystring": "0.2.0" } }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, "uuid": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", diff --git a/manual_test_websocket/package.json b/manual_test_websocket/package.json index 9039c28e9..2859fff2b 100644 --- a/manual_test_websocket/package.json +++ b/manual_test_websocket/package.json @@ -13,10 +13,11 @@ "author": "", "license": "MIT", "dependencies": { - "aws-sdk": "^2.453.0" + "aws-sdk": "^2.459.0" }, "devDependencies": { "chai": "^4.2.0", + "chai-http": "^4.3.0", "mocha": "^6.1.4", "serverless-dynamodb-local": "^0.2.37", "ws": "^6.2.1" diff --git a/manual_test_websocket/scripts/serverless..yml b/manual_test_websocket/scripts/serverless..yml index 07fb676c0..11339bd57 100644 --- a/manual_test_websocket/scripts/serverless..yml +++ b/manual_test_websocket/scripts/serverless..yml @@ -35,6 +35,12 @@ provider: - "arn:aws:execute-api:*:*:**/@connections/*" functions: + # http: + # handler: handler.http + # events: + # - http: + # path: /http + # method: GET connect: handler: handler.connect events: diff --git a/manual_test_websocket/test/e2e/ws.e2e.js b/manual_test_websocket/test/e2e/ws.e2e.js index 51c4d883b..20fd4534b 100644 --- a/manual_test_websocket/test/e2e/ws.e2e.js +++ b/manual_test_websocket/test/e2e/ws.e2e.js @@ -1,4 +1,6 @@ const chai = require('chai'); +const chaiHttp = require('chai-http'); +chai.use(chaiHttp); const expect = chai.expect; const endpoint=process.env.npm_config_endpoint||'ws://localhost:3000/dev'; const timeout=6000; @@ -41,6 +43,12 @@ describe('serverless', ()=>{ clients=[]; }); + // it('should request to upgade to WebSocket when receving an HTTP request', async ()=>{ + // const req=chai.request(`${endpoint.replace('ws://', 'http://').replace('wss://', 'https://')}`).keepOpen(); + // const res=await req.get(`/http`);//.set('Authorization', user.accessToken); + // expect(res).to.have.status(426); + // }); + it('should open a WebSocket', async ()=>{ const ws=await createWebSocket(); expect(ws).not.to.be.undefined; @@ -101,23 +109,23 @@ describe('serverless', ()=>{ await ws.receive1(); const c1=await createClient(); - expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c1.id, queryStringParameters:{}}}); + expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c1.id}}); const c2=await createClient(); - expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c2.id, queryStringParameters:{}}}); + expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c2.id}}); c2.ws.close(); expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'disconnect', info:{id:c2.id}}); const c3=await createClient(); - expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c3.id, queryStringParameters:{}}}); + expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c3.id}}); c1.ws.close(); expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'disconnect', info:{id:c1.id}}); c3.ws.close(); expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'disconnect', info:{id:c3.id}}); - }).timeout(6000); + }).timeout(8000); it('should be able to parse query string', async ()=>{ const now=''+Date.now(); @@ -125,8 +133,10 @@ describe('serverless', ()=>{ await ws.send(JSON.stringify({action:'registerListener'})); await ws.receive1(); - const c1=await createClient(`now=${now}&before=123456789`); - expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c1.id, queryStringParameters:{now, before:'123456789'}}}); - }); + const c1=await createClient(); + const c2=await createClient(`now=${now}&before=123456789`); + expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c1.id}}); + expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c2.id, queryStringParameters:{now, before:'123456789'}}}); + }).timeout(4000); }); }); \ No newline at end of file diff --git a/src/index.js b/src/index.js index d42a869a3..7c7a7b686 100644 --- a/src/index.js +++ b/src/index.js @@ -476,8 +476,10 @@ class Offline { const connectionId=utils.randomId(); console.log('connect '+connectionId); this.clients.set(ws, connectionId); + let params={requestContext:{eventType:'CONNECT', connectionId}} + if (0 { // if (ctx.to !== null) { From 53e9eef19c8fe18e901621a609f41d2f83308dbe Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Thu, 23 May 2019 23:08:08 +0300 Subject: [PATCH 04/71] Run WebSocket endpoint on a different port (http port+1) --- manual_test_websocket/package-lock.json | 6 +- manual_test_websocket/package.json | 2 +- manual_test_websocket/test/e2e/ws.e2e.js | 18 +- src/index.js | 1114 +++++++++++----------- 4 files changed, 596 insertions(+), 544 deletions(-) diff --git a/manual_test_websocket/package-lock.json b/manual_test_websocket/package-lock.json index adae25f87..70636121f 100644 --- a/manual_test_websocket/package-lock.json +++ b/manual_test_websocket/package-lock.json @@ -81,9 +81,9 @@ "dev": true }, "aws-sdk": { - "version": "2.459.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.459.0.tgz", - "integrity": "sha512-b1ziocvzv0NkV0oh5QRpGl28iYKfPqgcK52zvM02/sCmDH4YGuThC6ckFh/sKpXBYK7ToCg1St0LmtBK6siFbg==", + "version": "2.461.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.461.0.tgz", + "integrity": "sha512-nqRqlOaM92P6BTx/huq8FuowWNPiRRcpEKHvAQ2XTWTQUADx9HIP9KtbEzLpauxE4Er2reM0UYz9Kbtyke/3EQ==", "requires": { "buffer": "4.9.1", "events": "1.1.1", diff --git a/manual_test_websocket/package.json b/manual_test_websocket/package.json index 2859fff2b..b1497534b 100644 --- a/manual_test_websocket/package.json +++ b/manual_test_websocket/package.json @@ -13,7 +13,7 @@ "author": "", "license": "MIT", "dependencies": { - "aws-sdk": "^2.459.0" + "aws-sdk": "^2.461.0" }, "devDependencies": { "chai": "^4.2.0", diff --git a/manual_test_websocket/test/e2e/ws.e2e.js b/manual_test_websocket/test/e2e/ws.e2e.js index 20fd4534b..81e2bb2bd 100644 --- a/manual_test_websocket/test/e2e/ws.e2e.js +++ b/manual_test_websocket/test/e2e/ws.e2e.js @@ -2,7 +2,7 @@ const chai = require('chai'); const chaiHttp = require('chai-http'); chai.use(chaiHttp); const expect = chai.expect; -const endpoint=process.env.npm_config_endpoint||'ws://localhost:3000/dev'; +const endpoint=process.env.npm_config_endpoint||'ws://localhost:3001'; const timeout=6000; const WebSocketTester=require('../support/WebSocketTester'); @@ -43,11 +43,13 @@ describe('serverless', ()=>{ clients=[]; }); - // it('should request to upgade to WebSocket when receving an HTTP request', async ()=>{ - // const req=chai.request(`${endpoint.replace('ws://', 'http://').replace('wss://', 'https://')}`).keepOpen(); - // const res=await req.get(`/http`);//.set('Authorization', user.accessToken); - // expect(res).to.have.status(426); - // }); + it('should request to upgade to WebSocket when receving an HTTP request', async ()=>{ + const req=chai.request(`${endpoint.replace('ws://', 'http://').replace('wss://', 'https://')}`).keepOpen(); + let res=await req.get(`/${Date.now()}`);//.set('Authorization', user.accessToken); + expect(res).to.have.status(426); + res=await req.get(`/${Date.now()}/${Date.now()}`);//.set('Authorization', user.accessToken); + expect(res).to.have.status(426); + }); it('should open a WebSocket', async ()=>{ const ws=await createWebSocket(); @@ -125,7 +127,7 @@ describe('serverless', ()=>{ c3.ws.close(); expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'disconnect', info:{id:c3.id}}); - }).timeout(8000); + }).timeout(10000); it('should be able to parse query string', async ()=>{ const now=''+Date.now(); @@ -137,6 +139,6 @@ describe('serverless', ()=>{ const c2=await createClient(`now=${now}&before=123456789`); expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c1.id}}); expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c2.id, queryStringParameters:{now, before:'123456789'}}}); - }).timeout(4000); + }).timeout(5000); }); }); \ No newline at end of file diff --git a/src/index.js b/src/index.js index 7c7a7b686..c1cd69921 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,7 @@ const { exec } = require('child_process'); // External dependencies const Hapi = require('hapi'); +const h2o2 = require('h2o2'); const corsHeaders = require('hapi-cors-headers'); const crypto = require('crypto'); @@ -36,6 +37,7 @@ class Offline { this.options = options; this.exitCode = 0; this.clients = new Map; + this.wsActions = {}; this.commands = { offline: { @@ -91,6 +93,10 @@ class Offline { usage: 'Disable the timeout feature.', shortcut: 't', }, + binPath: { + usage: 'Path to the Serverless binary.', + shortcut: 'b', + }, noEnvironment: { usage: 'Turns off loading of your environment variables from serverless.yml. Allows the usage of tools such as PM2 or docker-compose.', }, @@ -231,6 +237,7 @@ class Offline { this._createRoutes(); // API Gateway emulation this._createResourceRoutes(); // HTTP Proxy defined in Resource this._create404Route(); // Not found handling + this._createWebSocket(); return this.server; } @@ -317,8 +324,7 @@ class Offline { }, }); - this.server.register(require('h2o2'), err => err && this.serverlessLog(err)); - + this.server.register(h2o2, err => err && this.serverlessLog(err)); const connectionOptions = { host: this.options.host, @@ -348,78 +354,53 @@ class Offline { // Passes the configuration object to the server this.server.connection(connectionOptions); - // Register WebSocket plugin - this.server.register(require('hapi-plugin-websocket'), err => err && this.serverlessLog(err)); - // Enable CORS preflight response this.server.ext('onPreResponse', corsHeaders); } - _createRoutes() { - let serviceRuntime = this.service.provider.runtime; - const defaultContentType = 'application/json'; - const apiKeys = this.service.provider.apiKeys; - const protectedRoutes = []; - - if (!serviceRuntime) { - throw new Error('Missing required property "runtime" for provider.'); - } - - if (typeof serviceRuntime !== 'string') { - throw new Error('Provider configuration property "runtime" wasn\'t a string.'); - } - - if (serviceRuntime === 'provided') { - if (this.options.providedRuntime) { - serviceRuntime = this.options.providedRuntime; - } - else { - throw new Error('Runtime "provided" is unsupported. Please add a --providedRuntime CLI option.'); - } - } + _createWebSocket() { + // Hapijs server creation + this.wsServer = new Hapi.Server({ + connections: { + router: { + stripTrailingSlash: !this.options.preserveTrailingSlash, // removes trailing slashes on incoming paths. + }, + }, + }); - if (!(serviceRuntime.startsWith('nodejs') || serviceRuntime.startsWith('python') || serviceRuntime.startsWith('ruby'))) { - this.printBlankLine(); - this.serverlessLog(`Warning: found unsupported runtime '${serviceRuntime}'`); + this.wsServer.register(h2o2, err => err && this.serverlessLog(err)); - return; - } + const connectionOptions = { + host: this.options.host, + port: this.options.port+1, + }; - // for simple API Key authentication model - if (apiKeys) { - this.serverlessLog(`Key with token: ${this.options.apiKey}`); + const httpsDir = this.options.httpsProtocol; - if (this.options.noAuth) { - this.serverlessLog('Authorizers are turned off. You do not need to use x-api-key header.'); - } - else { - this.serverlessLog('Remember to use x-api-key on the request headers'); - } + // HTTPS support + if (typeof httpsDir === 'string' && httpsDir.length > 0) { + connectionOptions.tls = { + key: fs.readFileSync(path.resolve(httpsDir, 'key.pem'), 'ascii'), + cert: fs.readFileSync(path.resolve(httpsDir, 'cert.pem'), 'ascii'), + }; } - const serviceInfo={serviceRuntime, defaultContentType, apiKeys, protectedRoutes, wsActions:{}}; - Object.keys(this.service.functions).forEach(key => { - - const fun = this.service.getFunction(key); - const funName = key; - const servicePath = path.join(this.serverless.config.servicePath, this.options.location); - const funOptions = functionHelper.getFunctionOptions(fun, key, servicePath, serviceRuntime); - - debugLog(`funOptions ${JSON.stringify(funOptions, null, 2)} `); - this.printBlankLine(); - debugLog(funName, 'runtime', serviceRuntime); - this.serverlessLog(`Routes for ${funName}:`); + connectionOptions.state = this.options.enforceSecureCookies ? { + isHttpOnly: true, + isSecure: true, + isSameSite: false, + } : { + isHttpOnly: false, + isSecure: false, + isSameSite: false, + }; - // Adds a route for each http endpoint - (fun.events && fun.events.length || this.serverlessLog('(none)')) && fun.events.forEach(event => { - // console.log(event) - if (event.http) this._createHttpEndpoint(serviceInfo, fun, funName, servicePath, funOptions, event); - else if (event.websocket) this._createWsEndpoint(serviceInfo, fun, funName, servicePath, funOptions, event); - else this.serverlessLog('(none)'); - }); - }); + // Passes the configuration object to the server + this.wsServer.connection(connectionOptions); - console.log(serviceInfo.wsActions); + // Enable CORS preflight response + this.wsServer.ext('onPreResponse', corsHeaders); + this.wsServer.register(require('hapi-plugin-websocket'), err => err && this.serverlessLog(err)); const plugin=this; const ApiGatewayManagementApi=class { @@ -444,14 +425,14 @@ class Offline { }; const doAction=(ws, connectionId, name, event, doDeafultAction, onError)=>{ - let action=serviceInfo.wsActions[name]; - if (!action&&doDeafultAction) action=serviceInfo.wsActions['$default']; + let action=this.wsActions[name]; + if (!action&&doDeafultAction) action=this.wsActions['$default']; if (!action) return; action.handler(event, {API:{ApiGatewayManagementApi}}, ()=>{}).catch(err=>ws.send(JSON.stringify({message:'Internal server error', connectionId, requestId:"1234567890"}))); }; - this.server.route({ - method: "POST", path: "/dev", + this.wsServer.route({ + method: 'POST', path: '/', config: { payload: { output: "data", parse: true, allow: "application/json" }, // auth: { mode: "required", strategy: "basic" }, @@ -504,23 +485,21 @@ class Offline { //return reply().code(204); } + }); + this.wsServer.route({ + method: 'GET', + path: '/{path*}', + handler: (request, reply)=>{ + const response = reply.response().hold(); + response.statusCode = 426; + // response.source = `[Serverless-Offline] Your λ handler '${funName}' timed out after ${funTimeout}ms.`; + /* eslint-enable no-param-reassign */ + response.send(); + } }); } - _createWsEndpoint(serviceInfo, fun, funName, servicePath, funOptions, event) { - // console.log('serviceInfo:'); - // console.log(serviceInfo); - // console.log('fun:'); - // console.log(fun); - // console.log('funName:'); - // console.log(funName); - // console.log('servicePath:'); - // console.log(servicePath); - // console.log('funOptions:'); - // console.log(funOptions); - // console.log('event:'); - // console.log(event); - + _createWsAction(fun, funName, servicePath, funOptions, event) { let handler; // The lambda function Object.assign(process.env, this.originalEnvironment); @@ -554,560 +533,627 @@ class Offline { const actionName=event.websocket.route; const action={funName, fun, funOptions, servicePath, handler}; - serviceInfo.wsActions[actionName]=action; + this.wsActions[actionName]=action; } - _createHttpEndpoint(serviceInfo, fun, funName, servicePath, funOptions, event) { - // Handle Simple http setup, ex. - http: GET users/index - if (typeof event.http === 'string') { - const split = event.http.split(' '); - event.http = { - path: split[1], - protectedRoutes: split[0], - }; + _createRoutes() { + let serviceRuntime = this.service.provider.runtime; + const defaultContentType = 'application/json'; + const apiKeys = this.service.provider.apiKeys; + const protectedRoutes = []; + + if (!serviceRuntime) { + throw new Error('Missing required property "runtime" for provider.'); } - // generate an enpoint via the endpoint class - const endpoint = new Endpoint(event.http, funOptions).generate(); + if (typeof serviceRuntime !== 'string') { + throw new Error('Provider configuration property "runtime" wasn\'t a string.'); + } - const integration = endpoint.integration || 'lambda-proxy'; - const requestBodyValidationModel = (['lambda', 'lambda-proxy'].includes(integration) - ? requestBodyValidator.getModel(this.service.custom, event.http, this.serverlessLog) - : null); - const epath = endpoint.path; - const method = endpoint.method.toUpperCase(); - const requestTemplates = endpoint.requestTemplates; + if (serviceRuntime === 'provided') { + if (this.options.providedRuntime) { + serviceRuntime = this.options.providedRuntime; + } + else { + throw new Error('Runtime "provided" is unsupported. Please add a --providedRuntime CLI option.'); + } + } - // Prefix must start and end with '/' BUT path must not end with '/' - let fullPath = this.options.prefix + (epath.startsWith('/') ? epath.slice(1) : epath); - if (fullPath !== '/' && fullPath.endsWith('/')) fullPath = fullPath.slice(0, -1); - fullPath = fullPath.replace(/\+}/g, '*}'); + if (!(serviceRuntime.startsWith('nodejs') || serviceRuntime.startsWith('python') || serviceRuntime.startsWith('ruby'))) { + this.printBlankLine(); + this.serverlessLog(`Warning: found unsupported runtime '${serviceRuntime}'`); - if (event.http.private) { - serviceInfo.protectedRoutes.push(`${method}#${fullPath}`); + return; } - this.serverlessLog(`${method} ${fullPath}${requestBodyValidationModel && !this.options.disableModelValidation ? ` - request body will be validated against ${requestBodyValidationModel.name}` : ''}`); - - // If the endpoint has an authorization function, create an authStrategy for the route - const authStrategyName = this.options.noAuth ? null : this._configureAuthorization(endpoint, funName, method, epath, servicePath, serviceInfo.serviceRuntime); + // for simple API Key authentication model + if (apiKeys) { + this.serverlessLog(`Key with token: ${this.options.apiKey}`); - let cors = null; - if (endpoint.cors) { - cors = { - origin: endpoint.cors.origins || this.options.corsConfig.origin, - headers: endpoint.cors.headers || this.options.corsConfig.headers, - credentials: endpoint.cors.credentials || this.options.corsConfig.credentials, - exposedHeaders: this.options.corsConfig.exposedHeaders, - }; + if (this.options.noAuth) { + this.serverlessLog('Authorizers are turned off. You do not need to use x-api-key header.'); + } + else { + this.serverlessLog('Remember to use x-api-key on the request headers'); + } } - // Route creation - const routeMethod = method === 'ANY' ? '*' : method; + Object.keys(this.service.functions).forEach(key => { - const state = this.options.disableCookieValidation ? { - parse: false, - failAction: 'ignore', - } : { - parse: true, - failAction: 'error', - }; + const fun = this.service.getFunction(key); + const funName = key; + const servicePath = path.join(this.serverless.config.servicePath, this.options.location); + const funOptions = functionHelper.getFunctionOptions(fun, key, servicePath, serviceRuntime); - const routeConfig = { - cors, - auth: authStrategyName, - timeout: { socket: false }, - state, - }; + debugLog(`funOptions ${JSON.stringify(funOptions, null, 2)} `); + this.printBlankLine(); + debugLog(funName, 'runtime', serviceRuntime); + this.serverlessLog(`Routes for ${funName}:`); - // skip HEAD routes as hapi will fail with 'Method name not allowed: HEAD ...' - // for more details, check https://github.com/dherault/serverless-offline/issues/204 - if (routeMethod === 'HEAD') { - this.serverlessLog('HEAD method event detected. Skipping HAPI server route mapping ...'); - return; - } + // Adds a route for each http endpoint + // eslint-disable-next-line + (fun.events && fun.events.length || this.serverlessLog('(none)')) && fun.events.forEach(event => { + if (event.websocket) { + this._createWsAction(fun, funName, servicePath, funOptions, event); + return; + } else if (!event.http) return this.serverlessLog('(none)'); + + // Handle Simple http setup, ex. - http: GET users/index + if (typeof event.http === 'string') { + const split = event.http.split(' '); + event.http = { + path: split[1], + method: split[0], + }; + } - if (routeMethod !== 'HEAD' && routeMethod !== 'GET') { - // maxBytes: Increase request size from 1MB default limit to 10MB. - // Cf AWS API GW payload limits. - routeConfig.payload = { parse: false, maxBytes: 1024 * 1024 * 10 }; - } + // generate an enpoint via the endpoint class + const endpoint = new Endpoint(event.http, funOptions).generate(); - this.server.route({ - method: routeMethod, - path: fullPath, - config: routeConfig, - handler: (request, reply) => { // Here we go - // Payload processing - const encoding = utils.detectEncoding(request); - - request.payload = request.payload && request.payload.toString(encoding); - request.rawPayload = request.payload; - - // Headers processing - // Hapi lowercases the headers whereas AWS does not - // so we recreate a custom headers object from the raw request - const headersArray = request.raw.req.rawHeaders; - - // During tests, `server.inject` uses *shot*, a package - // for performing injections that does not entirely mimick - // Hapi's usual request object. rawHeaders are then missing - // Hence the fallback for testing - - // Normal usage - if (headersArray) { - request.unprocessedHeaders = {}; - request.multiValueHeaders = {}; - - for (let i = 0; i < headersArray.length; i += 2) { - request.unprocessedHeaders[headersArray[i]] = headersArray[i + 1]; - request.multiValueHeaders[headersArray[i]] = (request.multiValueHeaders[headersArray[i]] || []).concat(headersArray[i + 1]); - } - } - // Lib testing - else { - request.unprocessedHeaders = request.headers; + const integration = endpoint.integration || 'lambda-proxy'; + const requestBodyValidationModel = (['lambda', 'lambda-proxy'].includes(integration) + ? requestBodyValidator.getModel(this.service.custom, event.http, this.serverlessLog) + : null); + const epath = endpoint.path; + const method = endpoint.method.toUpperCase(); + const requestTemplates = endpoint.requestTemplates; + + // Prefix must start and end with '/' BUT path must not end with '/' + let fullPath = this.options.prefix + (epath.startsWith('/') ? epath.slice(1) : epath); + if (fullPath !== '/' && fullPath.endsWith('/')) fullPath = fullPath.slice(0, -1); + fullPath = fullPath.replace(/\+}/g, '*}'); + + if (event.http.private) { + protectedRoutes.push(`${method}#${fullPath}`); } - // Incomming request message - this.printBlankLine(); - this.serverlessLog(`${method} ${request.path} (λ: ${funName})`); + this.serverlessLog(`${method} ${fullPath}${requestBodyValidationModel && !this.options.disableModelValidation ? ` - request body will be validated against ${requestBodyValidationModel.name}` : ''}`); - // Check for APIKey - if ((serviceInfo.protectedRoutes.includes(`${routeMethod}#${fullPath}`) || serviceInfo.protectedRoutes.includes(`ANY#${fullPath}`)) && !this.options.noAuth) { - const errorResponse = response => response({ message: 'Forbidden' }).code(403).type('application/json').header('x-amzn-ErrorType', 'ForbiddenException'); + // If the endpoint has an authorization function, create an authStrategy for the route + const authStrategyName = this.options.noAuth ? null : this._configureAuthorization(endpoint, funName, method, epath, servicePath, serviceRuntime); - if ('x-api-key' in request.headers) { - const requestToken = request.headers['x-api-key']; - if (requestToken !== this.options.apiKey) { - debugLog(`Method ${method} of function ${funName} token ${requestToken} not valid`); + let cors = null; + if (endpoint.cors) { + cors = { + origin: endpoint.cors.origins || this.options.corsConfig.origin, + headers: endpoint.cors.headers || this.options.corsConfig.headers, + credentials: endpoint.cors.credentials || this.options.corsConfig.credentials, + exposedHeaders: this.options.corsConfig.exposedHeaders, + }; + } - return errorResponse(reply); - } - } - else if (request.auth && request.auth.credentials && 'usageIdentifierKey' in request.auth.credentials) { - const usageIdentifierKey = request.auth.credentials.usageIdentifierKey; - if (usageIdentifierKey !== this.options.apiKey) { - debugLog(`Method ${method} of function ${funName} token ${usageIdentifierKey} not valid`); + // Route creation + const routeMethod = method === 'ANY' ? '*' : method; - return errorResponse(reply); - } - } - else { - debugLog(`Missing x-api-key on private function ${funName}`); + const state = this.options.disableCookieValidation ? { + parse: false, + failAction: 'ignore', + } : { + parse: true, + failAction: 'error', + }; - return errorResponse(reply); - } - } - // Shared mutable state is the root of all evil they say - const requestId = utils.randomId(); - this.requests[requestId] = { done: false }; - this.currentRequestId = requestId; + const routeConfig = { + cors, + auth: authStrategyName, + timeout: { socket: false }, + state, + }; - // Holds the response to do async op - const response = reply.response().hold(); - const contentType = request.mime || serviceInfo.defaultContentType; + // skip HEAD routes as hapi will fail with 'Method name not allowed: HEAD ...' + // for more details, check https://github.com/dherault/serverless-offline/issues/204 + if (routeMethod === 'HEAD') { + this.serverlessLog('HEAD method event detected. Skipping HAPI server route mapping ...'); - // default request template to '' if we don't have a definition pushed in from serverless or endpoint - const requestTemplate = typeof requestTemplates !== 'undefined' && integration === 'lambda' ? requestTemplates[contentType] : ''; + return; + } - // https://hapijs.com/api#route-configuration doesn't seem to support selectively parsing - // so we have to do it ourselves - const contentTypesThatRequirePayloadParsing = ['application/json', 'application/vnd.api+json']; - if (contentTypesThatRequirePayloadParsing.includes(contentType)) { - try { - request.payload = JSON.parse(request.payload); - } - catch (err) { - debugLog('error in converting request.payload to JSON:', err); - } + if (routeMethod !== 'HEAD' && routeMethod !== 'GET') { + // maxBytes: Increase request size from 1MB default limit to 10MB. + // Cf AWS API GW payload limits. + routeConfig.payload = { parse: false, maxBytes: 1024 * 1024 * 10 }; } - debugLog('requestId:', requestId); - debugLog('contentType:', contentType); - debugLog('requestTemplate:', requestTemplate); - debugLog('payload:', request.payload); - - /* HANDLER LAZY LOADING */ - - let handler; // The lambda function - Object.assign(process.env, this.originalEnvironment); - - try { - if (this.options.noEnvironment) { - // This evict errors in server when we use aws services like ssm - const baseEnvironment = { - AWS_REGION: 'dev', - }; - if (!process.env.AWS_PROFILE) { - baseEnvironment.AWS_ACCESS_KEY_ID = 'dev'; - baseEnvironment.AWS_SECRET_ACCESS_KEY = 'dev'; + this.server.route({ + method: routeMethod, + path: fullPath, + config: routeConfig, + handler: (request, reply) => { // Here we go + // Payload processing + const encoding = utils.detectEncoding(request); + + request.payload = request.payload && request.payload.toString(encoding); + request.rawPayload = request.payload; + + // Headers processing + // Hapi lowercases the headers whereas AWS does not + // so we recreate a custom headers object from the raw request + const headersArray = request.raw.req.rawHeaders; + + // During tests, `server.inject` uses *shot*, a package + // for performing injections that does not entirely mimick + // Hapi's usual request object. rawHeaders are then missing + // Hence the fallback for testing + + // Normal usage + if (headersArray) { + request.unprocessedHeaders = {}; + request.multiValueHeaders = {}; + + for (let i = 0; i < headersArray.length; i += 2) { + request.unprocessedHeaders[headersArray[i]] = headersArray[i + 1]; + request.multiValueHeaders[headersArray[i]] = (request.multiValueHeaders[headersArray[i]] || []).concat(headersArray[i + 1]); + } + } + // Lib testing + else { + request.unprocessedHeaders = request.headers; } - process.env = Object.assign(baseEnvironment, process.env); - } - else { - Object.assign( - process.env, - { AWS_REGION: this.service.provider.region }, - this.service.provider.environment, - this.service.functions[funName].environment - ); - } - process.env._HANDLER = fun.handler; - handler = functionHelper.createHandler(funOptions, this.options); - } - catch (err) { - return this._reply500(response, `Error while loading ${funName}`, err, requestId); - } + // Incomming request message + this.printBlankLine(); + this.serverlessLog(`${method} ${request.path} (λ: ${funName})`); - /* REQUEST TEMPLATE PROCESSING (event population) */ + // Check for APIKey + if ((protectedRoutes.includes(`${routeMethod}#${fullPath}`) || protectedRoutes.includes(`ANY#${fullPath}`)) && !this.options.noAuth) { + const errorResponse = response => response({ message: 'Forbidden' }).code(403).type('application/json').header('x-amzn-ErrorType', 'ForbiddenException'); - let event = {}; + if ('x-api-key' in request.headers) { + const requestToken = request.headers['x-api-key']; + if (requestToken !== this.options.apiKey) { + debugLog(`Method ${method} of function ${funName} token ${requestToken} not valid`); - if (integration === 'lambda') { - if (requestTemplate) { - try { - debugLog('_____ REQUEST TEMPLATE PROCESSING _____'); - // Velocity templating language parsing - const velocityContext = createVelocityContext(request, this.velocityContextOptions, request.payload || {}); - event = renderVelocityTemplateObject(requestTemplate, velocityContext); + return errorResponse(reply); + } + } + else if (request.auth && request.auth.credentials && 'usageIdentifierKey' in request.auth.credentials) { + const usageIdentifierKey = request.auth.credentials.usageIdentifierKey; + if (usageIdentifierKey !== this.options.apiKey) { + debugLog(`Method ${method} of function ${funName} token ${usageIdentifierKey} not valid`); + + return errorResponse(reply); + } + } + else { + debugLog(`Missing x-api-key on private function ${funName}`); + + return errorResponse(reply); + } } - catch (err) { - return this._reply500(response, `Error while parsing template "${contentType}" for ${funName}`, err, requestId); + // Shared mutable state is the root of all evil they say + const requestId = utils.randomId(); + this.requests[requestId] = { done: false }; + this.currentRequestId = requestId; + + // Holds the response to do async op + const response = reply.response().hold(); + const contentType = request.mime || defaultContentType; + + // default request template to '' if we don't have a definition pushed in from serverless or endpoint + const requestTemplate = typeof requestTemplates !== 'undefined' && integration === 'lambda' ? requestTemplates[contentType] : ''; + + // https://hapijs.com/api#route-configuration doesn't seem to support selectively parsing + // so we have to do it ourselves + const contentTypesThatRequirePayloadParsing = ['application/json', 'application/vnd.api+json']; + if (contentTypesThatRequirePayloadParsing.includes(contentType)) { + try { + request.payload = JSON.parse(request.payload); + } + catch (err) { + debugLog('error in converting request.payload to JSON:', err); + } } - } - else if (typeof request.payload === 'object') { - event = request.payload || {}; - } - } - else if (integration === 'lambda-proxy') { - event = createLambdaProxyContext(request, this.options, this.velocityContextOptions.stageVariables); - } - event.isOffline = true; + debugLog('requestId:', requestId); + debugLog('contentType:', contentType); + debugLog('requestTemplate:', requestTemplate); + debugLog('payload:', request.payload); - if (this.serverless.service.custom && this.serverless.service.custom.stageVariables) { - event.stageVariables = this.serverless.service.custom.stageVariables; - } - else if (integration !== 'lambda-proxy') { - event.stageVariables = {}; - } + /* HANDLER LAZY LOADING */ - debugLog('event:', event); + let handler; // The lambda function + Object.assign(process.env, this.originalEnvironment); - // We create the context, its callback (context.done/succeed/fail) will send the HTTP response - const lambdaContext = createLambdaContext(fun, (err, data, fromPromise) => { - // Everything in this block happens once the lambda function has resolved - debugLog('_____ HANDLER RESOLVED _____'); + try { + if (this.options.noEnvironment) { + // This evict errors in server when we use aws services like ssm + const baseEnvironment = { + AWS_REGION: 'dev', + }; + if (!process.env.AWS_PROFILE) { + baseEnvironment.AWS_ACCESS_KEY_ID = 'dev'; + baseEnvironment.AWS_SECRET_ACCESS_KEY = 'dev'; + } - // Timeout clearing if needed - if (this._clearTimeout(requestId)) return; + process.env = Object.assign(baseEnvironment, process.env); + } + else { + Object.assign( + process.env, + { AWS_REGION: this.service.provider.region }, + this.service.provider.environment, + this.service.functions[key].environment + ); + } + process.env._HANDLER = fun.handler; + handler = functionHelper.createHandler(funOptions, this.options); + } + catch (err) { + return this._reply500(response, `Error while loading ${funName}`, err); + } - // User should not call context.done twice - if (this.requests[requestId].done) { - this.printBlankLine(); - const warning = fromPromise - ? `Warning: handler '${funName}' returned a promise and also uses a callback!\nThis is problematic and might cause issues in your lambda.` - : `Warning: context.done called twice within handler '${funName}'!`; - this.serverlessLog(warning); - debugLog('requestId:', requestId); + /* REQUEST TEMPLATE PROCESSING (event population) */ - return; - } + let event = {}; - this.requests[requestId].done = true; - - let result = data; - let responseName = 'default'; - const responseContentType = endpoint.responseContentType; - const contentHandling = endpoint.contentHandling; - - /* RESPONSE SELECTION (among endpoint's possible responses) */ - - // Failure handling - let errorStatusCode = 0; - if (err) { - // Since the --useSeparateProcesses option loads the handler in - // a separate process and serverless-offline communicates with it - // over IPC, we are unable to catch JavaScript unhandledException errors - // when the handler code contains bad JavaScript. Instead, we "catch" - // it here and reply in the same way that we would have above when - // we lazy-load the non-IPC handler function. - if (this.options.useSeparateProcesses && err.ipcException) { - return this._reply500(response, `Error while loading ${funName}`, err, requestId); + if (integration === 'lambda') { + if (requestTemplate) { + try { + debugLog('_____ REQUEST TEMPLATE PROCESSING _____'); + // Velocity templating language parsing + const velocityContext = createVelocityContext(request, this.velocityContextOptions, request.payload || {}); + event = renderVelocityTemplateObject(requestTemplate, velocityContext); + } + catch (err) { + return this._reply500(response, `Error while parsing template "${contentType}" for ${funName}`, err); + } + } + else if (typeof request.payload === 'object') { + event = request.payload || {}; + } + } + else if (integration === 'lambda-proxy') { + event = createLambdaProxyContext(request, this.options, this.velocityContextOptions.stageVariables); } - const errorMessage = (err.message || err).toString(); + event.isOffline = true; - const re = /\[(\d{3})]/; - const found = errorMessage.match(re); - if (found && found.length > 1) { - errorStatusCode = found[1]; + if (this.serverless.service.custom && this.serverless.service.custom.stageVariables) { + event.stageVariables = this.serverless.service.custom.stageVariables; } - else { - errorStatusCode = '500'; + else if (integration !== 'lambda-proxy') { + event.stageVariables = {}; } - // Mocks Lambda errors - result = { - errorMessage, - errorType: err.constructor.name, - stackTrace: this._getArrayStackTrace(err.stack), - }; + debugLog('event:', event); - this.serverlessLog(`Failure: ${errorMessage}`); + // We create the context, its callback (context.done/succeed/fail) will send the HTTP response + const lambdaContext = createLambdaContext(fun, this.service.provider, (err, data, fromPromise) => { + // Everything in this block happens once the lambda function has resolved + debugLog('_____ HANDLER RESOLVED _____'); - if (result.stackTrace) { - debugLog(result.stackTrace.join('\n ')); - } + // User should not call context.done twice + if (this.requests[requestId].done) { + this.printBlankLine(); + const warning = fromPromise + ? `Warning: handler '${funName}' returned a promise and also uses a callback!\nThis is problematic and might cause issues in your lambda.` + : `Warning: context.done called twice within handler '${funName}'!`; + this.serverlessLog(warning); + debugLog('requestId:', requestId); - for (const key in endpoint.responses) { - if (key !== 'default' && errorMessage.match(`^${endpoint.responses[key].selectionPattern || key}$`)) { - responseName = key; - break; + return; + } + + this.requests[requestId].done = true; + + let result = data; + let responseName = 'default'; + const responseContentType = endpoint.responseContentType; + const contentHandling = endpoint.contentHandling; + + /* RESPONSE SELECTION (among endpoint's possible responses) */ + + // Failure handling + let errorStatusCode = 0; + if (err) { + // Since the --useSeparateProcesses option loads the handler in + // a separate process and serverless-offline communicates with it + // over IPC, we are unable to catch JavaScript unhandledException errors + // when the handler code contains bad JavaScript. Instead, we "catch" + // it here and reply in the same way that we would have above when + // we lazy-load the non-IPC handler function. + if (this.options.useSeparateProcesses && err.ipcException) { + return this._reply500(response, `Error while loading ${funName}`, err); + } + + const errorMessage = (err.message || err).toString(); + + const re = /\[(\d{3})]/; + const found = errorMessage.match(re); + if (found && found.length > 1) { + errorStatusCode = found[1]; + } + else { + errorStatusCode = '500'; + } + + // Mocks Lambda errors + result = { + errorMessage, + errorType: err.constructor.name, + stackTrace: this._getArrayStackTrace(err.stack), + }; + + this.serverlessLog(`Failure: ${errorMessage}`); + + if (result.stackTrace) { + debugLog(result.stackTrace.join('\n ')); + } + + for (const key in endpoint.responses) { + if (key !== 'default' && errorMessage.match(`^${endpoint.responses[key].selectionPattern || key}$`)) { + responseName = key; + break; + } + } } - } - } - debugLog(`Using response '${responseName}'`); - const chosenResponse = endpoint.responses[responseName]; + debugLog(`Using response '${responseName}'`); + const chosenResponse = endpoint.responses[responseName]; - /* RESPONSE PARAMETERS PROCCESSING */ + /* RESPONSE PARAMETERS PROCCESSING */ - const responseParameters = chosenResponse.responseParameters; + const responseParameters = chosenResponse.responseParameters; - if (responseParameters) { + if (responseParameters) { - const responseParametersKeys = Object.keys(responseParameters); + const responseParametersKeys = Object.keys(responseParameters); - debugLog('_____ RESPONSE PARAMETERS PROCCESSING _____'); - debugLog(`Found ${responseParametersKeys.length} responseParameters for '${responseName}' response`); + debugLog('_____ RESPONSE PARAMETERS PROCCESSING _____'); + debugLog(`Found ${responseParametersKeys.length} responseParameters for '${responseName}' response`); - responseParametersKeys.forEach(key => { + responseParametersKeys.forEach(key => { - // responseParameters use the following shape: "key": "value" - const value = responseParameters[key]; - const keyArray = key.split('.'); // eg: "method.response.header.location" - const valueArray = value.split('.'); // eg: "integration.response.body.redirect.url" + // responseParameters use the following shape: "key": "value" + const value = responseParameters[key]; + const keyArray = key.split('.'); // eg: "method.response.header.location" + const valueArray = value.split('.'); // eg: "integration.response.body.redirect.url" - debugLog(`Processing responseParameter "${key}": "${value}"`); + debugLog(`Processing responseParameter "${key}": "${value}"`); - // For now the plugin only supports modifying headers - if (key.startsWith('method.response.header') && keyArray[3]) { + // For now the plugin only supports modifying headers + if (key.startsWith('method.response.header') && keyArray[3]) { - const headerName = keyArray.slice(3).join('.'); - let headerValue; - debugLog('Found header in left-hand:', headerName); + const headerName = keyArray.slice(3).join('.'); + let headerValue; + debugLog('Found header in left-hand:', headerName); - if (value.startsWith('integration.response')) { - if (valueArray[2] === 'body') { + if (value.startsWith('integration.response')) { + if (valueArray[2] === 'body') { - debugLog('Found body in right-hand'); - headerValue = (valueArray[3] ? jsonPath(result, valueArray.slice(3).join('.')) : result).toString(); + debugLog('Found body in right-hand'); + headerValue = (valueArray[3] ? jsonPath(result, valueArray.slice(3).join('.')) : result).toString(); + + } + else { + this.printBlankLine(); + this.serverlessLog(`Warning: while processing responseParameter "${key}": "${value}"`); + this.serverlessLog(`Offline plugin only supports "integration.response.body[.JSON_path]" right-hand responseParameter. Found "${value}" instead. Skipping.`); + this.logPluginIssue(); + this.printBlankLine(); + } + } + else { + headerValue = value.match(/^'.*'$/) ? value.slice(1, -1) : value; // See #34 + } + // Applies the header; + debugLog(`Will assign "${headerValue}" to header "${headerName}"`); + response.header(headerName, headerValue); } else { this.printBlankLine(); this.serverlessLog(`Warning: while processing responseParameter "${key}": "${value}"`); - this.serverlessLog(`Offline plugin only supports "integration.response.body[.JSON_path]" right-hand responseParameter. Found "${value}" instead. Skipping.`); + this.serverlessLog(`Offline plugin only supports "method.response.header.PARAM_NAME" left-hand responseParameter. Found "${key}" instead. Skipping.`); this.logPluginIssue(); this.printBlankLine(); } - } - else { - headerValue = value.match(/^'.*'$/) ? value.slice(1, -1) : value; // See #34 - } - // Applies the header; - debugLog(`Will assign "${headerValue}" to header "${headerName}"`); - response.header(headerName, headerValue); - - } - else { - this.printBlankLine(); - this.serverlessLog(`Warning: while processing responseParameter "${key}": "${value}"`); - this.serverlessLog(`Offline plugin only supports "method.response.header.PARAM_NAME" left-hand responseParameter. Found "${key}" instead. Skipping.`); - this.logPluginIssue(); - this.printBlankLine(); + }); } - }); - } - let statusCode = 200; + let statusCode = 200; - if (integration === 'lambda') { + if (integration === 'lambda') { - const endpointResponseHeaders = (endpoint.response && endpoint.response.headers) || {}; + const endpointResponseHeaders = (endpoint.response && endpoint.response.headers) || {}; - Object.keys(endpointResponseHeaders) - .filter(key => typeof endpointResponseHeaders[key] === 'string' && /^'.*?'$/.test(endpointResponseHeaders[key])) - .forEach(key => response.header(key, endpointResponseHeaders[key].slice(1, endpointResponseHeaders[key].length - 1))); + Object.keys(endpointResponseHeaders) + .filter(key => typeof endpointResponseHeaders[key] === 'string' && /^'.*?'$/.test(endpointResponseHeaders[key])) + .forEach(key => response.header(key, endpointResponseHeaders[key].slice(1, endpointResponseHeaders[key].length - 1))); - /* LAMBDA INTEGRATION RESPONSE TEMPLATE PROCCESSING */ + /* LAMBDA INTEGRATION RESPONSE TEMPLATE PROCCESSING */ - // If there is a responseTemplate, we apply it to the result - const responseTemplates = chosenResponse.responseTemplates; + // If there is a responseTemplate, we apply it to the result + const responseTemplates = chosenResponse.responseTemplates; - if (typeof responseTemplates === 'object') { - const responseTemplatesKeys = Object.keys(responseTemplates); + if (typeof responseTemplates === 'object') { + const responseTemplatesKeys = Object.keys(responseTemplates); - if (responseTemplatesKeys.length) { + if (responseTemplatesKeys.length) { - // BAD IMPLEMENTATION: first key in responseTemplates - const responseTemplate = responseTemplates[responseContentType]; + // BAD IMPLEMENTATION: first key in responseTemplates + const responseTemplate = responseTemplates[responseContentType]; - if (responseTemplate && responseTemplate !== '\n') { + if (responseTemplate && responseTemplate !== '\n') { - debugLog('_____ RESPONSE TEMPLATE PROCCESSING _____'); - debugLog(`Using responseTemplate '${responseContentType}'`); + debugLog('_____ RESPONSE TEMPLATE PROCCESSING _____'); + debugLog(`Using responseTemplate '${responseContentType}'`); - try { - const reponseContext = createVelocityContext(request, this.velocityContextOptions, result); - result = renderVelocityTemplateObject({ root: responseTemplate }, reponseContext).root; - } - catch (error) { - this.serverlessLog(`Error while parsing responseTemplate '${responseContentType}' for lambda ${funName}:`); - console.log(error.stack); + try { + const reponseContext = createVelocityContext(request, this.velocityContextOptions, result); + result = renderVelocityTemplateObject({ root: responseTemplate }, reponseContext).root; + } + catch (error) { + this.serverlessLog(`Error while parsing responseTemplate '${responseContentType}' for lambda ${funName}:`); + console.log(error.stack); + } + } } } - } - } - /* LAMBDA INTEGRATION HAPIJS RESPONSE CONFIGURATION */ + /* LAMBDA INTEGRATION HAPIJS RESPONSE CONFIGURATION */ - statusCode = errorStatusCode !== 0 ? errorStatusCode : (chosenResponse.statusCode || 200); + statusCode = errorStatusCode !== 0 ? errorStatusCode : (chosenResponse.statusCode || 200); - if (!chosenResponse.statusCode) { - this.printBlankLine(); - this.serverlessLog(`Warning: No statusCode found for response "${responseName}".`); - } + if (!chosenResponse.statusCode) { + this.printBlankLine(); + this.serverlessLog(`Warning: No statusCode found for response "${responseName}".`); + } - response.header('Content-Type', responseContentType, { - override: false, // Maybe a responseParameter set it already. See #34 - }); + response.header('Content-Type', responseContentType, { + override: false, // Maybe a responseParameter set it already. See #34 + }); - response.statusCode = statusCode; + response.statusCode = statusCode; - if (contentHandling === 'CONVERT_TO_BINARY') { - response.encoding = 'binary'; - response.source = Buffer.from(result, 'base64'); - response.variety = 'buffer'; - } - else { - if (result && result.body && typeof result.body !== 'string') { - return this._reply500(response, 'According to the API Gateway specs, the body content must be stringified. Check your Lambda response and make sure you are invoking JSON.stringify(YOUR_CONTENT) on your body object', {}, requestId); + if (contentHandling === 'CONVERT_TO_BINARY') { + response.encoding = 'binary'; + response.source = Buffer.from(result, 'base64'); + response.variety = 'buffer'; + } + else { + if (result && result.body && typeof result.body !== 'string') { + return this._reply500(response, 'According to the API Gateway specs, the body content must be stringified. Check your Lambda response and make sure you are invoking JSON.stringify(YOUR_CONTENT) on your body object', {}); + } + response.source = result; + } } - response.source = result; - } - } - else if (integration === 'lambda-proxy') { + else if (integration === 'lambda-proxy') { - /* LAMBDA PROXY INTEGRATION HAPIJS RESPONSE CONFIGURATION */ + /* LAMBDA PROXY INTEGRATION HAPIJS RESPONSE CONFIGURATION */ - response.statusCode = statusCode = result.statusCode || 200; + response.statusCode = statusCode = result.statusCode || 200; - const headers = {}; - if (result.headers) { - Object.keys(result.headers).forEach(header => { - headers[header] = (headers[header] || []).concat(result.headers[header]); - }); - } - if (result.multiValueHeaders) { - Object.keys(result.multiValueHeaders).forEach(header => { - headers[header] = (headers[header] || []).concat(result.multiValueHeaders[header]); - }); - } + const headers = {}; + if (result.headers) { + Object.keys(result.headers).forEach(header => { + headers[header] = (headers[header] || []).concat(result.headers[header]); + }); + } + if (result.multiValueHeaders) { + Object.keys(result.multiValueHeaders).forEach(header => { + headers[header] = (headers[header] || []).concat(result.multiValueHeaders[header]); + }); + } - debugLog('headers', headers); + debugLog('headers', headers); - Object.keys(headers).forEach(header => { - if (header.toLowerCase() === 'set-cookie') { - headers[header].forEach(headerValue => { - const cookieName = headerValue.slice(0, headerValue.indexOf('=')); - const cookieValue = headerValue.slice(headerValue.indexOf('=') + 1); - reply.state(cookieName, cookieValue, { encoding: 'none', strictHeader: false }); - }); - } - else { - headers[header].forEach(headerValue => { - // it looks like Hapi doesn't support multiple headers with the same name, - // appending values is the closest we can come to the AWS behavior. - response.header(header, headerValue, { append: true }); + Object.keys(headers).forEach(header => { + if (header.toLowerCase() === 'set-cookie') { + headers[header].forEach(headerValue => { + const cookieName = headerValue.slice(0, headerValue.indexOf('=')); + const cookieValue = headerValue.slice(headerValue.indexOf('=') + 1); + reply.state(cookieName, cookieValue, { encoding: 'none', strictHeader: false }); + }); + } + else { + headers[header].forEach(headerValue => { + // it looks like Hapi doesn't support multiple headers with the same name, + // appending values is the closest we can come to the AWS behavior. + response.header(header, headerValue, { append: true }); + }); + } }); - } - }); - response.header('Content-Type', 'application/json', { override: false, duplicate: false }); + response.header('Content-Type', 'application/json', { override: false, duplicate: false }); - if (typeof result.body !== 'undefined') { - if (result.isBase64Encoded) { - response.encoding = 'binary'; - response.source = Buffer.from(result.body, 'base64'); - response.variety = 'buffer'; - } - else { - if (result.body && typeof result.body !== 'string') { - return this._reply500(response, 'According to the API Gateway specs, the body content must be stringified. Check your Lambda response and make sure you are invoking JSON.stringify(YOUR_CONTENT) on your body object', {}, requestId); + if (typeof result.body !== 'undefined') { + if (result.isBase64Encoded) { + response.encoding = 'binary'; + response.source = Buffer.from(result.body, 'base64'); + response.variety = 'buffer'; + } + else { + if (result.body && typeof result.body !== 'string') { + return this._reply500(response, 'According to the API Gateway specs, the body content must be stringified. Check your Lambda response and make sure you are invoking JSON.stringify(YOUR_CONTENT) on your body object', {}); + } + response.source = result.body; + } } - response.source = result.body; } - } - } - // Log response - let whatToLog = result; + // Log response + let whatToLog = result; - try { - whatToLog = JSON.stringify(result); - } - catch (error) { - // nothing - } - finally { - if (this.options.printOutput) this.serverlessLog(err ? `Replying ${statusCode}` : `[${statusCode}] ${whatToLog}`); - debugLog('requestId:', requestId); - } + try { + whatToLog = JSON.stringify(result); + } + catch (error) { + // nothing + } + finally { + if (this.options.printOutput) this.serverlessLog(err ? `Replying ${statusCode}` : `[${statusCode}] ${whatToLog}`); + debugLog('requestId:', requestId); + } - // Bon voyage! - response.send(); - }); + // Bon voyage! + response.send(); + }); - // Now we are outside of createLambdaContext, so this happens before the handler gets called: + // Now we are outside of createLambdaContext, so this happens before the handler gets called: - // We cannot use Hapijs's timeout feature because the logic above can take a significant time, so we implement it ourselves - this.requests[requestId].timeout = this.options.noTimeout ? null : setTimeout( - this._replyTimeout.bind(this, response, funName, funOptions.funTimeout, requestId), - funOptions.funTimeout - ); + // We cannot use Hapijs's timeout feature because the logic above can take a significant time, so we implement it ourselves + this.requests[requestId].timeout = this.options.noTimeout ? null : setTimeout( + this._replyTimeout.bind(this, response, funName, funOptions.funTimeout, requestId), + funOptions.funTimeout + ); - // If request body validation is enabled, validate body against the request model. - if (requestBodyValidationModel && !this.options.disableModelValidation) { - try { - requestBodyValidator.validate(requestBodyValidationModel, event.body); - } - catch (error) { - // When request body validation fails, APIG will return back 400 as detailed in: - // https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-method-request-validation.html - return this._replyError(400, response, `Invalid request body for '${funName}' handler`, error, requestId); - } - } + // If request body validation is enabled, validate body against the request model. + if (requestBodyValidationModel && !this.options.disableModelValidation) { + try { + requestBodyValidator.validate(requestBodyValidationModel, event.body); + } + catch (error) { + // When request body validation fails, APIG will return back 400 as detailed in: + // https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-method-request-validation.html + return this._replyError(400, response, `Invalid request body for '${funName}' handler`, error); + } + } - // Finally we call the handler - debugLog('_____ CALLING HANDLER _____'); - try { - const x = handler(event, lambdaContext, lambdaContext.done); + // Finally we call the handler + debugLog('_____ CALLING HANDLER _____'); + try { + const x = handler(event, lambdaContext, lambdaContext.done); - // Promise support - if (!this.requests[requestId].done) { - if (x && typeof x.then === 'function' && typeof x.catch === 'function') x.then(lambdaContext.succeed).catch(lambdaContext.fail); - else if (x instanceof Error) lambdaContext.fail(x); - } - } - catch (error) { - return this._reply500(response, `Uncaught error in your '${funName}' handler`, error, requestId); - } - }, + // Promise support + if (!this.requests[requestId].done) { + if (x && typeof x.then === 'function' && typeof x.catch === 'function') x.then(lambdaContext.succeed).catch(lambdaContext.fail); + else if (x instanceof Error) lambdaContext.fail(x); + } + } + catch (error) { + return this._reply500(response, `Uncaught error in your '${funName}' handler`, error); + } + finally { + setTimeout(() => { + this._clearTimeout(requestId); + delete this.requests[requestId]; + }, 0); + } + }, + }); + }); }); } @@ -1176,8 +1222,8 @@ class Offline { } // All done, we can listen to incomming requests - _listen() { - return new Promise((resolve, reject) => { + async _listen() { + await new Promise((resolve, reject) => { this.server.start(err => { if (err) return reject(err); @@ -1187,6 +1233,17 @@ class Offline { resolve(this.server); }); }); + await new Promise((resolve, reject) => { + this.wsServer.start(err => { + if (err) return reject(err); + + this.printBlankLine(); + this.serverlessLog(`Offline listening on ws${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port+1}`); + + resolve(this.wsServer); + }); + }); + return this.server; } end() { @@ -1197,12 +1254,7 @@ class Offline { } // Bad news - _replyError(responseCode, response, message, err, requestId) { - - if (this._clearTimeout(requestId)) return; - - this.requests[requestId].done = true; - + _replyError(responseCode, response, message, err) { const stackTrace = this._getArrayStackTrace(err.stack); this.serverlessLog(message); @@ -1228,16 +1280,14 @@ class Offline { response.send(); } - _reply500(response, message, err, requestId) { + _reply500(response, message, err) { // APIG replies 200 by default on failures - this._replyError(200, response, message, err, requestId); + this._replyError(200, response, message, err); } _replyTimeout(response, funName, funTimeout, requestId) { if (this.currentRequestId !== requestId) return; - this.requests[requestId].done = true; - this.serverlessLog(`Replying timeout after ${funTimeout}ms`); /* eslint-disable no-param-reassign */ response.statusCode = 503; @@ -1247,8 +1297,7 @@ class Offline { } _clearTimeout(requestId) { - const timeout = this.requests[requestId].timeout; - if (timeout && timeout._called) return true; + const { timeout } = this.requests[requestId]; clearTimeout(timeout); } @@ -1351,6 +1400,7 @@ class Offline { } _logAndExit() { + // eslint-disable-next-line console.log.apply(null, arguments); process.exit(0); } From 602ac7bde7b3f6a32c338cb7bd4924f4c7159410 Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Sun, 26 May 2019 15:35:24 +0300 Subject: [PATCH 05/71] GWAPI REST API suppoprt POST to /@connections/{connectionId} to send message to client at connectionId. --- manual_test_websocket/package-lock.json | 18 +++++-- manual_test_websocket/package.json | 4 +- manual_test_websocket/test/e2e/ws.e2e.js | 66 +++++++++++++++++++----- src/index.js | 37 +++++++++++-- 4 files changed, 104 insertions(+), 21 deletions(-) diff --git a/manual_test_websocket/package-lock.json b/manual_test_websocket/package-lock.json index 70636121f..299183d86 100644 --- a/manual_test_websocket/package-lock.json +++ b/manual_test_websocket/package-lock.json @@ -81,9 +81,9 @@ "dev": true }, "aws-sdk": { - "version": "2.461.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.461.0.tgz", - "integrity": "sha512-nqRqlOaM92P6BTx/huq8FuowWNPiRRcpEKHvAQ2XTWTQUADx9HIP9KtbEzLpauxE4Er2reM0UYz9Kbtyke/3EQ==", + "version": "2.463.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.463.0.tgz", + "integrity": "sha512-bl0S6mxA2o8Hpy9LsMcPC9kVnrL4+B4qayiMlNMrp+8gcEMzJSMzfrUDX4SCWsqGYT25sZn8TDqTGJp7XIh3yw==", "requires": { "buffer": "4.9.1", "events": "1.1.1", @@ -96,6 +96,18 @@ "xml2js": "0.4.19" } }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "dev": true + }, + "awscred": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/awscred/-/awscred-1.4.2.tgz", + "integrity": "sha512-j3Vehf6PCFzzPZKkzEcj0Y2QO8w8UBbgobnl3DwHMiAE9A2mfJxTkq3cX4UNWHmrTAR0rj5BC/ts90Ok4Pg6rw==", + "dev": true + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", diff --git a/manual_test_websocket/package.json b/manual_test_websocket/package.json index b1497534b..ad504a854 100644 --- a/manual_test_websocket/package.json +++ b/manual_test_websocket/package.json @@ -13,9 +13,11 @@ "author": "", "license": "MIT", "dependencies": { - "aws-sdk": "^2.461.0" + "aws-sdk": "^2.463.0" }, "devDependencies": { + "aws4": "^1.8.0", + "awscred": "^1.4.2", "chai": "^4.2.0", "chai-http": "^4.3.0", "mocha": "^6.1.4", diff --git a/manual_test_websocket/test/e2e/ws.e2e.js b/manual_test_websocket/test/e2e/ws.e2e.js index 81e2bb2bd..0513cc327 100644 --- a/manual_test_websocket/test/e2e/ws.e2e.js +++ b/manual_test_websocket/test/e2e/ws.e2e.js @@ -2,14 +2,16 @@ const chai = require('chai'); const chaiHttp = require('chai-http'); chai.use(chaiHttp); const expect = chai.expect; +const aws4 = require('aws4'); +const awscred = require('awscred'); const endpoint=process.env.npm_config_endpoint||'ws://localhost:3001'; -const timeout=6000; +const timeout = 10000; const WebSocketTester=require('../support/WebSocketTester'); describe('serverless', ()=>{ describe('with WebSocket support', ()=>{ - let clients=[]; + let clients=[]; let req=null; let cred=null; const createWebSocket=async (qs)=>{ const ws=new WebSocketTester(); let url=endpoint; @@ -25,6 +27,13 @@ describe('serverless', ()=>{ const id=JSON.parse(json).info.id; return {ws, id}; }; + before(async ()=>{ + req=chai.request(`${endpoint.replace('ws://', 'http://').replace('wss://', 'https://')}`).keepOpen(); + // req=chai.request('http://localhost:3001/dev').keepOpen(); + cred=await new Promise((resolve, reject)=>{ + awscred.loadCredentials(function(err, data) { if (err) reject(err); else resolve(data); }); + }); + }); beforeEach(()=>{ clients=[]; @@ -49,32 +58,32 @@ describe('serverless', ()=>{ expect(res).to.have.status(426); res=await req.get(`/${Date.now()}/${Date.now()}`);//.set('Authorization', user.accessToken); expect(res).to.have.status(426); - }); + }).timeout(timeout); it('should open a WebSocket', async ()=>{ const ws=await createWebSocket(); expect(ws).not.to.be.undefined; - }); + }).timeout(timeout); it('should receive client connection info', async ()=>{ const ws=await createWebSocket(); ws.send(JSON.stringify({action:'getClientInfo'})); const clientInfo=JSON.parse(await ws.receive1()); expect(clientInfo).to.deep.equal({action:'update', event:'client-info', info:{id:clientInfo.info.id}}); - }); + }).timeout(timeout); it('should call default handler when no such action exists', async ()=>{ const ws=await createWebSocket(); const payload=JSON.stringify({action:'action'+Date.now()}); ws.send(payload); expect(await ws.receive1()).to.equal(`Error: No Supported Action in Payload '${payload}'`); - }); + }).timeout(timeout); it('should call default handler when no action provided', async ()=>{ const ws=await createWebSocket(); ws.send(JSON.stringify({hello:'world'})); expect(await ws.receive1()).to.equal(`Error: No Supported Action in Payload '{"hello":"world"}'`); - }); + }).timeout(timeout); it('should send & receive data', async ()=>{ const c1=await createClient(); @@ -83,27 +92,27 @@ describe('serverless', ()=>{ c1.ws.send(JSON.stringify({action:'send', data:'Hello World!', clients:[c1.id, c3.id]})); expect(await c1.ws.receive1()).to.equal('Hello World!'); expect(await c3.ws.receive1()).to.equal('Hello World!'); - }).timeout(timeout); + }).timeout(6000); it('should response when having an internal server error', async ()=>{ const conn=await createClient(); conn.ws.send(JSON.stringify({action:'makeError'})); const res=JSON.parse(await conn.ws.receive1()); expect(res).to.deep.equal({message:'Internal server error', connectionId:conn.id, requestId:res.requestId}); - }); + }).timeout(timeout); it('should response with only the last action when there are more than one in the serverless.yml file', async ()=>{ const ws=await createWebSocket(); ws.send(JSON.stringify({action:'makeMultiCalls'})); const res=JSON.parse(await ws.receive1()); expect(res).to.deep.equal({action:'update', event:'made-call-2'}); - }); + }).timeout(timeout); it('should not send to non existing client', async ()=>{ const c1=await createClient(); c1.ws.send(JSON.stringify({action:'send', data:'Hello World!', clients:["non-existing-id"]})); expect(await c1.ws.receive1()).to.equal('Error: Could not Send all Messages'); - }); + }).timeout(timeout); it('should connect & disconnect', async ()=>{ const ws=await createWebSocket(); @@ -127,7 +136,7 @@ describe('serverless', ()=>{ c3.ws.close(); expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'disconnect', info:{id:c3.id}}); - }).timeout(10000); + }).timeout(timeout); it('should be able to parse query string', async ()=>{ const now=''+Date.now(); @@ -139,6 +148,37 @@ describe('serverless', ()=>{ const c2=await createClient(`now=${now}&before=123456789`); expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c1.id}}); expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c2.id, queryStringParameters:{now, before:'123456789'}}}); - }).timeout(5000); + }).timeout(timeout); + + it('should be able to receive messages to via REST API', async ()=>{ + const c1=await createClient(); + const c2=await createClient(); + const url=new URL(endpoint); + const signature = {service: 'execute-api', host:url.host, path:`${url.pathname}/@connections/${c2.id}`, method: 'POST', body:'Hello World!', headers:{'Content-Type':'text/plain'/*'application/text'*/}}; + aws4.sign(signature, {accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey}); + const res=await req.post(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers['Authorization']).set('Content-Type', signature.headers['Content-Type']).send('Hello World!'); + expect(res).to.have.status(200); + expect(await c2.ws.receive1()).to.equal('Hello World!'); + }).timeout(timeout); + + it('should receive error code when sending to non existing client via REST API', async ()=>{ + const c='aJz0Md6VoAMCIbQ='; + const url=new URL(endpoint); + const signature = {service: 'execute-api', host:url.host, path:`${url.pathname}/@connections/${c}`, method: 'POST', body:'Hello World!', headers:{'Content-Type':'text/plain'/*'application/text'*/}}; + aws4.sign(signature, {accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey}); + const res=await req.post(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers['Authorization']).set('Content-Type', signature.headers['Content-Type']).send('Hello World!'); + expect(res).to.have.status(410); + }).timeout(timeout); + + // UNABLE TO TEST HIS SCENARIO BECAUSE AWS DOESN'T RETURN ANYTHING + // it('should not receive anything when POSTing nothing', async ()=>{ + // const c1=await createClient(); + // const url=new URL(endpoint); + // const signature = {service: 'execute-api', host:url.host, path:`${url.pathname}/@connections/${c1.id}`, method: 'POST'/*, body:'Hello World!'*/, headers:{'Content-Type':'text/plain'/*'application/text'*/}}; + // aws4.sign(signature, {accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey}); + // const res=await req.post(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers['Authorization']).set('Content-Type', signature.headers['Content-Type']).send(/*'Hello World!'*/); + // expect(res).to.have.status(200); + // }).timeout(timeout); + }); }); \ No newline at end of file diff --git a/src/index.js b/src/index.js index c1cd69921..57c113fe1 100644 --- a/src/index.js +++ b/src/index.js @@ -455,7 +455,6 @@ class Offline { const queryStringParameters = parseQuery(req.url); const connectionId=utils.randomId(); - console.log('connect '+connectionId); this.clients.set(ws, connectionId); let params={requestContext:{eventType:'CONNECT', connectionId}} if (0 { const { initially, ws } = request.websocket(); - console.log(`initially:${initially}`) if (!request.payload||initially) return; const connectionId=this.clients.get(ws); doAction(ws, connectionId, request.payload.action||'$default', {body:JSON.stringify(request.payload), requestContext:{domainName:'localhost', stage:'local', connectionId}}, true); @@ -496,7 +493,39 @@ class Offline { /* eslint-enable no-param-reassign */ response.send(); } - }); + }); + this.wsServer.route({ + method: 'POST', + path: '/@connections/{connectionId}', + handler: (request, reply)=>{ + const getByValue=(map, searchValue)=>{ + for (let [key, value] of map.entries()) { + if (value === searchValue) + return key; + } + return undefined; + }; + + const postToConnection=({ws,Data})=>{ + if (!ws||!Data) return {promise:()=>{ return new Promise((resolve, reject)=>{setTimeout(()=>{reject()}, 10)})}}; + ws.send(Data); + return {promise:()=>{ return new Promise((resolve, reject)=>{setTimeout(()=>{resolve()}, 10)})}}; + }; + + const response = reply.response().hold(); + const ws=getByValue(this.clients, request.params.connectionId); + if (!ws) { + response.statusCode = 410; + response.send(); + return; + } + postToConnection({ws, Data:request.payload}); + // response.source = `[Serverless-Offline] Your λ handler '${funName}' timed out after ${funTimeout}ms.`; + /* eslint-enable no-param-reassign */ + response.statusCode = 200; + response.send(); + } + }); } _createWsAction(fun, funName, servicePath, funOptions, event) { From c4c6ea65a9ee98575a451a0fe29a1ee3afc6a456 Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Mon, 27 May 2019 09:59:10 +0300 Subject: [PATCH 06/71] Changed the way to get function to post-to-connection Here's the new way: const endpoint=event.requestContext.domainName+'/'+event.requestContext.stage; const apiVersion='2018-11-29'; let API=null; if (!process.env.IS_OFFLINE) { API = require('aws-sdk'); require('aws-sdk/clients/apigatewaymanagementapi'); } else { API = require('serverless-offline').AWS; } return new API.ApiGatewayManagementApi({ apiVersion, endpoint }); --- manual_test_websocket/handler.js | 6 ++- src/index.js | 74 ++++++++++++++++++-------------- 2 files changed, 45 insertions(+), 35 deletions(-) diff --git a/manual_test_websocket/handler.js b/manual_test_websocket/handler.js index c61952d95..989de383e 100644 --- a/manual_test_websocket/handler.js +++ b/manual_test_websocket/handler.js @@ -95,10 +95,12 @@ module.exports.deleteListener = async (event, context) => { const newAWSApiGatewayManagementApi=(event, context)=>{ const endpoint=event.requestContext.domainName+'/'+event.requestContext.stage; const apiVersion='2018-11-29'; - let API=context.API; + let API=null; if (!process.env.IS_OFFLINE) { - API = require('aws-sdk'); + API = AWS; require('aws-sdk/clients/apigatewaymanagementapi'); + } else { + API = require('serverless-offline').AWS; } return new API.ApiGatewayManagementApi({ apiVersion, endpoint }); }; diff --git a/src/index.js b/src/index.js index 57c113fe1..f0d8a821a 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,7 @@ const fs = require('fs'); const path = require('path'); const { exec } = require('child_process'); +const http = require('http'); // External dependencies const Hapi = require('hapi'); @@ -165,6 +166,43 @@ class Offline { this.serverlessLog('https://github.com/dherault/serverless-offline/issues'); } + static AWS = { + ApiGatewayManagementApi:class { + constructor(apiVersion, client) { + } + + postToConnection({ConnectionId,Data}) { + return { + promise:()=>{ + return new Promise((resolve, reject)=>{ + const options = { + hostname: 'localhost', + port: 3001, + path: `/@connections/${ConnectionId}`, + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + 'Content-Length': Buffer.byteLength(Data) + } + }; + + const req = http.request(options, (res) => { + if (200===res.statusCode) resolve(); else reject(); + }); + + req.on('error', (e) => { + reject(); + }); + + req.write(Data); + req.end(); + }); + } + } + } + } + }; + // Entry point for the plugin (sls offline) start() { this._checkVersion(); @@ -402,33 +440,11 @@ class Offline { this.wsServer.ext('onPreResponse', corsHeaders); this.wsServer.register(require('hapi-plugin-websocket'), err => err && this.serverlessLog(err)); - const plugin=this; - const ApiGatewayManagementApi=class { - constructor(apiVersion, client) { - - } - - getByValue(map, searchValue) { - for (let [key, value] of map.entries()) { - if (value === searchValue) - return key; - } - return undefined; - } - - postToConnection({ConnectionId,Data}) { - const ws=this.getByValue(plugin.clients, ConnectionId); - if (!ws||!Data) return {promise:()=>{ return new Promise((resolve, reject)=>{setTimeout(()=>{reject()}, 10)})}}; - ws.send(Data); - return {promise:()=>{ return new Promise((resolve, reject)=>{setTimeout(()=>{resolve()}, 10)})}}; - } - }; - const doAction=(ws, connectionId, name, event, doDeafultAction, onError)=>{ let action=this.wsActions[name]; if (!action&&doDeafultAction) action=this.wsActions['$default']; if (!action) return; - action.handler(event, {API:{ApiGatewayManagementApi}}, ()=>{}).catch(err=>ws.send(JSON.stringify({message:'Internal server error', connectionId, requestId:"1234567890"}))); + action.handler(event, {}, ()=>{}).catch(err=>{ if (/*OPEN*/1===ws.readyState) ws.send(JSON.stringify({message:'Internal server error', connectionId, requestId:"1234567890"})); }); }; this.wsServer.route({ @@ -505,12 +521,6 @@ class Offline { } return undefined; }; - - const postToConnection=({ws,Data})=>{ - if (!ws||!Data) return {promise:()=>{ return new Promise((resolve, reject)=>{setTimeout(()=>{reject()}, 10)})}}; - ws.send(Data); - return {promise:()=>{ return new Promise((resolve, reject)=>{setTimeout(()=>{resolve()}, 10)})}}; - }; const response = reply.response().hold(); const ws=getByValue(this.clients, request.params.connectionId); @@ -519,10 +529,8 @@ class Offline { response.send(); return; } - postToConnection({ws, Data:request.payload}); - // response.source = `[Serverless-Offline] Your λ handler '${funName}' timed out after ${funTimeout}ms.`; - /* eslint-enable no-param-reassign */ - response.statusCode = 200; + if (!request.payload) return; + ws.send(request.payload); response.send(); } }); From 10c377dac25d520d958b21d38cb0b0615c7275c5 Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Mon, 27 May 2019 12:00:24 +0300 Subject: [PATCH 07/71] Default timeout is 1000ms and can set timeout value via --timeout= --- manual_test_websocket/test/e2e/ws.e2e.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/manual_test_websocket/test/e2e/ws.e2e.js b/manual_test_websocket/test/e2e/ws.e2e.js index 0513cc327..4c5c787e7 100644 --- a/manual_test_websocket/test/e2e/ws.e2e.js +++ b/manual_test_websocket/test/e2e/ws.e2e.js @@ -4,8 +4,8 @@ chai.use(chaiHttp); const expect = chai.expect; const aws4 = require('aws4'); const awscred = require('awscred'); -const endpoint=process.env.npm_config_endpoint||'ws://localhost:3001'; -const timeout = 10000; +const endpoint = process.env.npm_config_endpoint||'ws://localhost:3001'; +const timeout = process.env.npm_config_timeout||1000; const WebSocketTester=require('../support/WebSocketTester'); @@ -92,7 +92,7 @@ describe('serverless', ()=>{ c1.ws.send(JSON.stringify({action:'send', data:'Hello World!', clients:[c1.id, c3.id]})); expect(await c1.ws.receive1()).to.equal('Hello World!'); expect(await c3.ws.receive1()).to.equal('Hello World!'); - }).timeout(6000); + }).timeout(timeout); it('should response when having an internal server error', async ()=>{ const conn=await createClient(); From bca81cdaeac32a23380f50311768cb93880be14c Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Mon, 27 May 2019 19:46:20 +0300 Subject: [PATCH 08/71] Minor Improved logs 1. Action of routes. 2. Listening on WS message sending via REST API. --- src/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/index.js b/src/index.js index f0d8a821a..c35af106c 100644 --- a/src/index.js +++ b/src/index.js @@ -571,6 +571,7 @@ class Offline { const actionName=event.websocket.route; const action={funName, fun, funOptions, servicePath, handler}; this.wsActions[actionName]=action; + this.serverlessLog(`Action '${event.websocket.route}'`); } _createRoutes() { @@ -1277,6 +1278,9 @@ class Offline { this.printBlankLine(); this.serverlessLog(`Offline listening on ws${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port+1}`); + this.printBlankLine(); + this.serverlessLog(`Offline listening on http${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port+1}/@connections/{connectionId}`); + resolve(this.wsServer); }); }); From da8c749d3232e2d9d6869174624f92376af5e70b Mon Sep 17 00:00:00 2001 From: computerpunc Date: Mon, 27 May 2019 22:22:08 +0300 Subject: [PATCH 09/71] Update README with new ways of sending messages to clients --- manual_test_websocket/README.md | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/manual_test_websocket/README.md b/manual_test_websocket/README.md index d1ad7bddb..8074c2f6e 100644 --- a/manual_test_websocket/README.md +++ b/manual_test_websocket/README.md @@ -30,24 +30,30 @@ To start AWS DynamoDB locally (can run only after first deploying locally): `sls ## Usage Assumption - In order to send messages back to clients -`const newAWSApiGatewayManagementApi=(event, context)=>{` + +`POST http://localhost:3001/@connections/{connectionId}` -` const endpoint=event.requestContext.domainName+'/'+event.requestContext.stage;` +Or, -` const apiVersion='2018-11-29';` +`const endpoint=event.requestContext.domainName+'/'+event.requestContext.stage;` -` let API=context.API;` +`const apiVersion='2018-11-29';` -` if (!process.env.IS_OFFLINE) {` +`let API=null;` -` API = require('aws-sdk');` +`if (!process.env.IS_OFFLINE) {` -` require('aws-sdk/clients/apigatewaymanagementapi');` +` API = require('aws-sdk');` -` }` +` require('aws-sdk/clients/apigatewaymanagementapi');` -` return new API.ApiGatewayManagementApi({ apiVersion, endpoint });` +`} else {` + +` API = require('serverless-offline').AWS;` + +`}` + +`new API.ApiGatewayManagementApi({ apiVersion, endpoint });` -`};` From f527670cc1e9fe9bffb9beea4468383abc890cc7 Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Tue, 28 May 2019 15:40:56 +0300 Subject: [PATCH 10/71] Leftovers from merge --- manual_test_websocket/README.md | 2 +- package-lock.json | 164 +++++++++++++++++++++++++++++--- package.json | 4 +- 3 files changed, 154 insertions(+), 16 deletions(-) diff --git a/manual_test_websocket/README.md b/manual_test_websocket/README.md index 8074c2f6e..2d7390e62 100644 --- a/manual_test_websocket/README.md +++ b/manual_test_websocket/README.md @@ -26,7 +26,7 @@ To start AWS DynamoDB locally (can run only after first deploying locally): `sls ## Testing on AWS -`npm --endpoint={WebSocket endpoint URL on AWS} run test` +`npm --endpoint={WebSocket endpoint URL on AWS} --timeout={timeout in ms} run test` ## Usage Assumption - In order to send messages back to clients diff --git a/package-lock.json b/package-lock.json index 1cd00a6a3..2178251cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -335,6 +335,11 @@ "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", "dev": true }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, "axobject-query": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.2.tgz", @@ -447,7 +452,6 @@ "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "dev": true, "requires": { "core-js": "^2.4.0", "regenerator-runtime": "^0.11.0" @@ -525,6 +529,11 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "bignumber.js": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-6.0.0.tgz", + "integrity": "sha512-x247jIuy60/+FtMRvscqfxtVHQf8AGx2hm9c6btkgC0x/hp9yt+teISNhvF8WlwRkCc5yF2fDECH8SIMe8j+GA==" + }, "boom": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/boom/-/boom-7.3.0.tgz", @@ -737,7 +746,7 @@ }, "catbox-memory": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/catbox-memory/-/catbox-memory-2.0.4.tgz", + "resolved": "http://registry.npmjs.org/catbox-memory/-/catbox-memory-2.0.4.tgz", "integrity": "sha1-Qz4lWQLK9UIz0ShkKcj03xToItU=", "requires": { "hoek": "4.x.x" @@ -750,6 +759,22 @@ } } }, + "cbor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-4.0.0.tgz", + "integrity": "sha512-SWtnNIThYI4bM1cg/5AKj2oKDsrFOmQb5W4pr6jaIlbsOfl/aLHJADx9hVkAqUX4PR3iDZLp8f9S6QPP9VPXxg==", + "requires": { + "bignumber.js": "^6.0", + "commander": "^2.14.1", + "json-text-sequence": "^0.1", + "nofilter": "^0.0.3" + } + }, + "cbor-js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cbor-js/-/cbor-js-0.1.0.tgz", + "integrity": "sha1-yAzmEg84fo+qdDcN/aIdlluPx/k=" + }, "chai": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", @@ -861,8 +886,7 @@ "commander": { "version": "2.20.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", - "dev": true + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" }, "concat-map": { "version": "0.0.1", @@ -892,7 +916,7 @@ }, "content": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/content/-/content-3.0.7.tgz", + "resolved": "http://registry.npmjs.org/content/-/content-3.0.7.tgz", "integrity": "sha512-LXtnSnvE+Z1Cjpa3P9gh9kb396qV4MqpfwKy777BOSF8n6nw2vAi03tHNl0/XRqZUyzVzY/+nMXOZVnEapWzdg==", "requires": { "boom": "5.x.x" @@ -916,8 +940,7 @@ "core-js": { "version": "2.6.6", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.6.tgz", - "integrity": "sha512-Mt/LaAym54NXnrjEMdo918cT2h70tqb/Yl7T3uPHQHRm5SxVoqlKmerUy4mL11k8saSBDWQ7ULIHxmeFyT3pfg==", - "dev": true + "integrity": "sha512-Mt/LaAym54NXnrjEMdo918cT2h70tqb/Yl7T3uPHQHRm5SxVoqlKmerUy4mL11k8saSBDWQ7ULIHxmeFyT3pfg==" }, "create-error-class": { "version": "3.0.2", @@ -1025,6 +1048,11 @@ "object-keys": "^1.0.12" } }, + "delimit-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/delimit-stream/-/delimit-stream-0.1.0.tgz", + "integrity": "sha1-m4MZR3wOX4rrPONXrjBfwl6hzSs=" + }, "depcheck": { "version": "0.6.11", "resolved": "https://registry.npmjs.org/depcheck/-/depcheck-0.6.11.tgz", @@ -1277,6 +1305,18 @@ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", "dev": true }, + "encodr": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/encodr/-/encodr-1.0.7.tgz", + "integrity": "sha512-3GtpSnmDeNDOMcvcjV+rurzQBy+y802omzQn46iex3mJ0NDsHKw1ay2kOdIqGYTsohwE1nH6FJCz8IXzpZZOlQ==", + "requires": { + "babel-runtime": "6.26.0", + "cbor": "4.0.0", + "cbor-js": "0.1.0", + "msgpack-lite": "0.1.26", + "utf8": "3.0.0" + } + }, "end-of-stream": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", @@ -1631,6 +1671,16 @@ "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", "dev": true }, + "event-lite": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.2.tgz", + "integrity": "sha512-HnSYx1BsJ87/p6swwzv+2v6B4X+uxUteoDfRxsAb1S1BePzQqOLevVmkdA15GHJVd9A9Ok6wygUR18Hu0YeV9g==" + }, + "eventemitter3": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.0.1.tgz", + "integrity": "sha512-QOCPu979MMWX9XNlfRZoin+Wm+bK1SP7vv3NGUniYwuSJK/+cPA10blMaeRgzg31RvoSFk6FsCDVa4vNryBTGA==" + }, "execa": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", @@ -2013,6 +2063,33 @@ "resolved": "https://registry.npmjs.org/hapi-cors-headers/-/hapi-cors-headers-1.0.3.tgz", "integrity": "sha512-U/y+kpVLUJ0y86fEk8yleou9C1T5wFopcWQjuxKdMXzCcymTjfSqGz59waqvngUs1SbeXav/y8Ga9C0G0L1MGg==" }, + "hapi-plugin-websocket": { + "version": "1.2.19", + "resolved": "https://registry.npmjs.org/hapi-plugin-websocket/-/hapi-plugin-websocket-1.2.19.tgz", + "integrity": "sha512-I1ExBMXlRnlyKEaUCKOdt8e7jpvYrQ/cP5laX9c9nO/yMTPeL91ZSUi8RVkKFVVjqwJcAgyOpXXptcZv9ugMNw==", + "requires": { + "boom": "7.1.1", + "hoek": "5.0.2", + "urijs": "1.19.1", + "websocket-framed": "1.0.14", + "ws": "5.1.0" + }, + "dependencies": { + "boom": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-7.1.1.tgz", + "integrity": "sha512-qwEARHTliqgEQiVkzKkkbLt3q0vRPIW60VRZ8zRnbjsm7INkPe9NxfAYDDYLZOdhxyUHa1gIe639Cx7t6RH/4A==", + "requires": { + "hoek": "5.x.x" + } + }, + "hoek": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.2.tgz", + "integrity": "sha512-NA10UYP9ufCtY2qYGkZktcQXwVyYK4zK0gkaFSB96xhtlo6V8tKXdQgx8eHolQTRemaW0uLn8BhjhwqrOU+QLQ==" + } + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -2130,6 +2207,11 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -2227,6 +2309,11 @@ } } }, + "int64-buffer": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/int64-buffer/-/int64-buffer-0.1.10.tgz", + "integrity": "sha1-J3siiofZWtd30HwTgyAiQGpHNCM=" + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -2417,8 +2504,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isemail": { "version": "2.2.1", @@ -2493,6 +2579,14 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, + "json-text-sequence": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/json-text-sequence/-/json-text-sequence-0.1.1.tgz", + "integrity": "sha1-py8hfcSvxGKf/1/rME3BvVGi89I=", + "requires": { + "delimit-stream": "0.1.0" + } + }, "jsonpath-plus": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-0.19.0.tgz", @@ -2882,7 +2976,7 @@ }, "mimos": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/mimos/-/mimos-3.0.3.tgz", + "resolved": "http://registry.npmjs.org/mimos/-/mimos-3.0.3.tgz", "integrity": "sha1-uRCQcq03jCty9qAQHEPd+ys2ZB8=", "requires": { "hoek": "4.x.x", @@ -3019,6 +3113,17 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" }, + "msgpack-lite": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/msgpack-lite/-/msgpack-lite-0.1.26.tgz", + "integrity": "sha1-3TxQsm8FnyXn7e42REGDWOKprYk=", + "requires": { + "event-lite": "^0.1.1", + "ieee754": "^1.1.8", + "int64-buffer": "^0.1.9", + "isarray": "^1.0.0" + } + }, "mute-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", @@ -3039,7 +3144,7 @@ }, "nigel": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/nigel/-/nigel-2.0.2.tgz", + "resolved": "http://registry.npmjs.org/nigel/-/nigel-2.0.2.tgz", "integrity": "sha1-k6GGb7DFLYc5CqdeKxYfS1x15bE=", "requires": { "hoek": "4.x.x", @@ -3093,6 +3198,11 @@ "semver": "^5.7.0" } }, + "nofilter": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-0.0.3.tgz", + "integrity": "sha1-JB40IHgXeoaTowQ+g/N1Z+J2QQw=" + }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -3891,8 +4001,7 @@ "regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" }, "regexpp": { "version": "2.0.1", @@ -4522,6 +4631,11 @@ "punycode": "^2.1.0" } }, + "urijs": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.1.tgz", + "integrity": "sha512-xVrGVi94ueCJNrBSTjWqjvtgvl3cyOTThp2zaMaFNGp3F542TR6sM3f2o8RqZl+AwteClSVmoCyt0ka4RjQOQg==" + }, "url-parse-lax": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", @@ -4531,6 +4645,11 @@ "prepend-http": "^1.0.1" } }, + "utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", + "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==" + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -4548,7 +4667,7 @@ }, "vise": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/vise/-/vise-2.0.2.tgz", + "resolved": "http://registry.npmjs.org/vise/-/vise-2.0.2.tgz", "integrity": "sha1-awjo+0y3bjpQzW3Q7DczjoEaDTk=", "requires": { "hoek": "4.x.x" @@ -4567,6 +4686,15 @@ "integrity": "sha1-oW0CXrkxvQO1LzCMrtD0D86+lTI=", "dev": true }, + "websocket-framed": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/websocket-framed/-/websocket-framed-1.0.14.tgz", + "integrity": "sha512-gx/t6+hrWc9TASmetg6Q7G2MeOFqG910xVmhtGutInsdY3OdghdyzOOjQcUsfzbqQ6ZxERvjS67ooxPFKx8YCg==", + "requires": { + "encodr": "1.0.7", + "eventemitter3": "3.0.1" + } + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -4713,6 +4841,14 @@ "signal-exit": "^3.0.2" } }, + "ws": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.1.0.tgz", + "integrity": "sha512-7KU/qkUXtJW9aa5WRKlo0puE1ejEoAgDb0D/Pt+lWpTkKF7Kp+MqFOtwNFwnuiYeeDpFjp0qyMniE84OjKIEqQ==", + "requires": { + "async-limiter": "~1.0.0" + } + }, "xdg-basedir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", diff --git a/package.json b/package.json index 3230f6f3d..67c89bfe5 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,8 @@ "Alessandro Palumbo (https://github.com/apalumbo)", "Selcuk Cihan (https://github.com/selcukcihan)", "G Roques (https://github.com/gbroques)", - "Dustin Belliston (https://github.com/dwbelliston)" + "Dustin Belliston (https://github.com/dwbelliston)", + "Ram Hardy (https://github.com/computerpunc)" ], "dependencies": { "boom": "^7.3.0", @@ -121,6 +122,7 @@ "h2o2": "^6.1.0", "hapi": "^16.7.0", "hapi-cors-headers": "^1.0.3", + "hapi-plugin-websocket": "^1.2.19", "js-string-escape": "^1.0.1", "jsonpath-plus": "^0.19.0", "jsonschema": "^1.2.4", From 4ec82ad945c05e3aed785231dfa754ee411c1944 Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Tue, 28 May 2019 17:04:48 +0300 Subject: [PATCH 11/71] Fix lint errors --- src/index.js | 138 +++++++++++++++++++++++++-------------------------- 1 file changed, 69 insertions(+), 69 deletions(-) diff --git a/src/index.js b/src/index.js index ade9d940d..7a47f1f3a 100644 --- a/src/index.js +++ b/src/index.js @@ -37,7 +37,7 @@ class Offline { this.serverlessLog = serverless.cli.log.bind(serverless.cli); this.options = options; this.exitCode = 0; - this.clients = new Map; + this.clients = new Map(); this.wsActions = {}; this.commands = { @@ -168,13 +168,13 @@ class Offline { static AWS = { ApiGatewayManagementApi:class { - constructor(apiVersion, client) { - } + // constructor(apiVersion, client) { + // } - postToConnection({ConnectionId,Data}) { + postToConnection({ ConnectionId, Data }) { return { - promise:()=>{ - return new Promise((resolve, reject)=>{ + promise:() => { + const p = new Promise((resolve, reject) => { const options = { hostname: 'localhost', port: 3001, @@ -182,25 +182,27 @@ class Offline { method: 'POST', headers: { 'Content-Type': 'text/plain', - 'Content-Length': Buffer.byteLength(Data) - } + 'Content-Length': Buffer.byteLength(Data), + }, }; - const req = http.request(options, (res) => { - if (200===res.statusCode) resolve(); else reject(); + const req = http.request(options, res => { + if (res.statusCode === 200) resolve(); else reject(); }); - req.on('error', (e) => { + req.on('error', () => { reject(); }); req.write(Data); req.end(); }); - } - } + + return p; + }, + }; } - } + }, }; // Entry point for the plugin (sls offline) when running 'sls offline start' @@ -419,7 +421,7 @@ class Offline { const connectionOptions = { host: this.options.host, - port: this.options.port+1, + port: this.options.port + 1, }; const httpsDir = this.options.httpsProtocol; @@ -449,99 +451,94 @@ class Offline { this.wsServer.ext('onPreResponse', corsHeaders); this.wsServer.register(require('hapi-plugin-websocket'), err => err && this.serverlessLog(err)); - const doAction=(ws, connectionId, name, event, doDeafultAction, onError)=>{ - let action=this.wsActions[name]; - if (!action&&doDeafultAction) action=this.wsActions['$default']; + const doAction = (ws, connectionId, name, event, doDeafultAction/* , onError */) => { + let action = this.wsActions[name]; + if (!action && doDeafultAction) action = this.wsActions.$default; if (!action) return; - action.handler(event, {}, ()=>{}).catch(err=>{ if (/*OPEN*/1===ws.readyState) ws.send(JSON.stringify({message:'Internal server error', connectionId, requestId:"1234567890"})); }); + action.handler(event, {}, () => {}).catch(() => { + if (ws.readyState === /* OPEN */1) ws.send(JSON.stringify({ message:'Internal server error', connectionId, requestId:'1234567890' })); + }); }; this.wsServer.route({ - method: 'POST', path: '/', + method: 'POST', + path: '/', config: { - payload: { output: "data", parse: true, allow: "application/json" }, - // auth: { mode: "required", strategy: "basic" }, + payload: { output: 'data', parse: true, allow: 'application/json' }, plugins: { websocket: { only: true, initially: true, - // subprotocol: "quux/1.0", connect: ({ ws, req }) => { - const parseQuery=(queryString)=>{ - const query = {}; const parts=req.url.split('?'); - if (2>parts.length) return {}; - var pairs = parts[1].split('&'); - pairs.forEach((pair)=>{ - const kv = pair.split('='); - query[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1] || ''); + const parseQuery = queryString => { + const query = {}; const parts = queryString.split('?'); + if (parts.length < 2) return {}; + const pairs = parts[1].split('&'); + pairs.forEach(pair => { + const kv = pair.split('='); + query[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1] || ''); }); + return query; }; const queryStringParameters = parseQuery(req.url); - const connectionId=utils.randomId(); + const connectionId = utils.randomId(); this.clients.set(ws, connectionId); - let params={requestContext:{eventType:'CONNECT', connectionId}} - if (0 0) params = { queryStringParameters, ...params }; doAction(ws, connectionId, '$connect', params); }, - disconnect: ({ ctx, ws }) => { - // if (ctx.to !== null) { - // clearTimeout(ctx.to) - // ctx.to = null - // } - const connectionId=this.clients.get(ws); - this.clients.delete(ws); - - doAction(ws, connectionId, '$disconnect', {requestContext:{eventType:'DISCONNECT', connectionId}}); - } - } - } + disconnect: ({ ws }) => { + const connectionId = this.clients.get(ws); + this.clients.delete(ws); + + doAction(ws, connectionId, '$disconnect', { requestContext: { eventType:'DISCONNECT', connectionId } }); + }, + }, + }, }, - handler: (request, reply) => { + handler: request /* , reply */ => { const { initially, ws } = request.websocket(); - if (!request.payload||initially) return; - const connectionId=this.clients.get(ws); - doAction(ws, connectionId, request.payload.action||'$default', {body:JSON.stringify(request.payload), requestContext:{domainName:'localhost', stage:'local', connectionId}}, true); - - //return reply().code(204); - } + if (!request.payload || initially) return; + const connectionId = this.clients.get(ws); + doAction(ws, connectionId, request.payload.action || '$default', { body:JSON.stringify(request.payload), requestContext: { domainName:'localhost', stage:'local', connectionId } }, true); + }, }); this.wsServer.route({ method: 'GET', path: '/{path*}', - handler: (request, reply)=>{ + handler: (request, reply) => { const response = reply.response().hold(); response.statusCode = 426; - // response.source = `[Serverless-Offline] Your λ handler '${funName}' timed out after ${funTimeout}ms.`; - /* eslint-enable no-param-reassign */ response.send(); - } + }, }); this.wsServer.route({ method: 'POST', path: '/@connections/{connectionId}', - handler: (request, reply)=>{ - const getByValue=(map, searchValue)=>{ - for (let [key, value] of map.entries()) { - if (value === searchValue) - return key; + handler: (request, reply) => { + const getByValue = (map, searchValue) => { + for (const [key, value] of map.entries()) { + if (value === searchValue) return key; } + return undefined; }; const response = reply.response().hold(); - const ws=getByValue(this.clients, request.params.connectionId); + const ws = getByValue(this.clients, request.params.connectionId); if (!ws) { response.statusCode = 410; response.send(); + return; } if (!request.payload) return; ws.send(request.payload); response.send(); - } + }, }); } @@ -577,9 +574,9 @@ class Offline { return this.serverlessLog(`Error while loading ${funName}`, err); } - const actionName=event.websocket.route; - const action={funName, fun, funOptions, servicePath, handler}; - this.wsActions[actionName]=action; + const actionName = event.websocket.route; + const action = { funName, fun, funOptions, servicePath, handler }; + this.wsActions[actionName] = action; this.serverlessLog(`Action '${event.websocket.route}'`); } @@ -642,8 +639,10 @@ class Offline { (fun.events && fun.events.length || this.serverlessLog('(none)')) && fun.events.forEach(event => { if (event.websocket) { this._createWsAction(fun, funName, servicePath, funOptions, event); + return; - } else if (!event.http) return this.serverlessLog('(none)'); + } + if (!event.http) return this.serverlessLog('(none)'); // Handle Simple http setup, ex. - http: GET users/index if (typeof event.http === 'string') { @@ -1291,14 +1290,15 @@ class Offline { if (err) return reject(err); this.printBlankLine(); - this.serverlessLog(`Offline listening on ws${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port+1}`); + this.serverlessLog(`Offline listening on ws${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port + 1}`); this.printBlankLine(); - this.serverlessLog(`Offline listening on http${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port+1}/@connections/{connectionId}`); + this.serverlessLog(`Offline listening on http${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port + 1}/@connections/{connectionId}`); resolve(this.wsServer); }); }); + return this.server; } From 4e61aa53dd55313912596c9cd6372944aba8f1e8 Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Tue, 28 May 2019 18:10:51 +0300 Subject: [PATCH 12/71] Fix static AWS = { ... } --- src/index.js | 72 +++++++++++++++++++++++++++------------------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/src/index.js b/src/index.js index 7a47f1f3a..3ece13a96 100644 --- a/src/index.js +++ b/src/index.js @@ -166,43 +166,45 @@ class Offline { this.serverlessLog('https://github.com/dherault/serverless-offline/issues'); } - static AWS = { - ApiGatewayManagementApi:class { - // constructor(apiVersion, client) { - // } - - postToConnection({ ConnectionId, Data }) { - return { - promise:() => { - const p = new Promise((resolve, reject) => { - const options = { - hostname: 'localhost', - port: 3001, - path: `/@connections/${ConnectionId}`, - method: 'POST', - headers: { - 'Content-Type': 'text/plain', - 'Content-Length': Buffer.byteLength(Data), - }, - }; - - const req = http.request(options, res => { - if (res.statusCode === 200) resolve(); else reject(); - }); - - req.on('error', () => { - reject(); + static get AWS() { + return { + ApiGatewayManagementApi:class { + // constructor(apiVersion, client) { + // } + + postToConnection({ ConnectionId, Data }) { + return { + promise:() => { + const p = new Promise((resolve, reject) => { + const options = { + hostname: 'localhost', + port: 3001, + path: `/@connections/${ConnectionId}`, + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + 'Content-Length': Buffer.byteLength(Data), + }, + }; + + const req = http.request(options, res => { + if (res.statusCode === 200) resolve(); else reject(); + }); + + req.on('error', () => { + reject(); + }); + + req.write(Data); + req.end(); }); - req.write(Data); - req.end(); - }); - - return p; - }, - }; - } - }, + return p; + }, + }; + } + }, + } }; // Entry point for the plugin (sls offline) when running 'sls offline start' From 289e8f5f8ef835f230120cac06beffe275abb910 Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Tue, 28 May 2019 18:51:18 +0300 Subject: [PATCH 13/71] Fix 2 lint errors --- src/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 3ece13a96..c2de2f0f0 100644 --- a/src/index.js +++ b/src/index.js @@ -204,8 +204,8 @@ class Offline { }; } }, - } - }; + }; + } // Entry point for the plugin (sls offline) when running 'sls offline start' start() { From ca34ccbb5dd0ed3b2094b257bea3541d84cf433b Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Wed, 29 May 2019 08:18:56 +0300 Subject: [PATCH 14/71] Removed require('aws-sdk/clients/apigatewaymanagementapi'); Doesn't seem to be needed anymore as a workaround in aws-sdk. --- manual_test_websocket/README.md | 7 ++++--- manual_test_websocket/handler.js | 1 - manual_test_websocket/package-lock.json | 6 +++--- manual_test_websocket/package.json | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/manual_test_websocket/README.md b/manual_test_websocket/README.md index 2d7390e62..ff1ce7ea5 100644 --- a/manual_test_websocket/README.md +++ b/manual_test_websocket/README.md @@ -45,15 +45,16 @@ Or, ` API = require('aws-sdk');` -` require('aws-sdk/clients/apigatewaymanagementapi');` - `} else {` ` API = require('serverless-offline').AWS;` `}` -`new API.ApiGatewayManagementApi({ apiVersion, endpoint });` +`const apiGM=new API.ApiGatewayManagementApi({ apiVersion, endpoint });` + + +`apiGM.postToConnection({ConnectionId, Data});` diff --git a/manual_test_websocket/handler.js b/manual_test_websocket/handler.js index 989de383e..af3ff0c0d 100644 --- a/manual_test_websocket/handler.js +++ b/manual_test_websocket/handler.js @@ -98,7 +98,6 @@ const newAWSApiGatewayManagementApi=(event, context)=>{ let API=null; if (!process.env.IS_OFFLINE) { API = AWS; - require('aws-sdk/clients/apigatewaymanagementapi'); } else { API = require('serverless-offline').AWS; } diff --git a/manual_test_websocket/package-lock.json b/manual_test_websocket/package-lock.json index 299183d86..9239a96c1 100644 --- a/manual_test_websocket/package-lock.json +++ b/manual_test_websocket/package-lock.json @@ -81,9 +81,9 @@ "dev": true }, "aws-sdk": { - "version": "2.463.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.463.0.tgz", - "integrity": "sha512-bl0S6mxA2o8Hpy9LsMcPC9kVnrL4+B4qayiMlNMrp+8gcEMzJSMzfrUDX4SCWsqGYT25sZn8TDqTGJp7XIh3yw==", + "version": "2.464.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.464.0.tgz", + "integrity": "sha512-DsJ/V/Eaazio5klO49IXIgnDpBcObgOunFs0KsUbdPz5yxvznZQiez9LqTcaj0SaCS7zsrT9K5p+Jtbt60z1SA==", "requires": { "buffer": "4.9.1", "events": "1.1.1", diff --git a/manual_test_websocket/package.json b/manual_test_websocket/package.json index ad504a854..07ee591bf 100644 --- a/manual_test_websocket/package.json +++ b/manual_test_websocket/package.json @@ -13,7 +13,7 @@ "author": "", "license": "MIT", "dependencies": { - "aws-sdk": "^2.463.0" + "aws-sdk": "^2.464.0" }, "devDependencies": { "aws4": "^1.8.0", From 2f3dfbd41dcc988759099afe95e55e9da64bd257 Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Thu, 30 May 2019 08:51:06 +0300 Subject: [PATCH 15/71] Removed the need for require('serverless-offline').AWS Use plain vanilla AWS SDK: let endpoint=event.apiGatewayUrl; if (!endpoint) endpoint = event.requestContext.domainName+'/'+event.requestContext.stage; const apiVersion='2018-11-29'; return new AWS.ApiGatewayManagementApi({ apiVersion, endpoint }); --- manual_test_websocket/README.md | 19 +---- manual_test_websocket/handler.js | 10 +-- manual_test_websocket/package-lock.json | 6 +- manual_test_websocket/package.json | 2 +- manual_test_websocket/test/e2e/ws.e2e.js | 2 +- src/index.js | 97 ++++++++++++------------ 6 files changed, 60 insertions(+), 76 deletions(-) diff --git a/manual_test_websocket/README.md b/manual_test_websocket/README.md index ff1ce7ea5..b907d45ff 100644 --- a/manual_test_websocket/README.md +++ b/manual_test_websocket/README.md @@ -29,31 +29,20 @@ To start AWS DynamoDB locally (can run only after first deploying locally): `sls `npm --endpoint={WebSocket endpoint URL on AWS} --timeout={timeout in ms} run test` -## Usage Assumption - In order to send messages back to clients +## Usage in order to send messages back to clients `POST http://localhost:3001/@connections/{connectionId}` Or, -`const endpoint=event.requestContext.domainName+'/'+event.requestContext.stage;` +`let endpoint=event.apiGatewayUrl;` -`const apiVersion='2018-11-29';` - -`let API=null;` - -`if (!process.env.IS_OFFLINE) {` - -` API = require('aws-sdk');` +`if (!endpoint) endpoint = event.requestContext.domainName+'/'+event.requestContext.stage;` -`} else {` - -` API = require('serverless-offline').AWS;` - -`}` +`const apiVersion='2018-11-29';` `const apiGM=new API.ApiGatewayManagementApi({ apiVersion, endpoint });` - `apiGM.postToConnection({ConnectionId, Data});` diff --git a/manual_test_websocket/handler.js b/manual_test_websocket/handler.js index af3ff0c0d..263bd8334 100644 --- a/manual_test_websocket/handler.js +++ b/manual_test_websocket/handler.js @@ -94,14 +94,10 @@ module.exports.deleteListener = async (event, context) => { const newAWSApiGatewayManagementApi=(event, context)=>{ const endpoint=event.requestContext.domainName+'/'+event.requestContext.stage; + let endpoint=event.apiGatewayUrl; + if (!endpoint) endpoint = event.requestContext.domainName+'/'+event.requestContext.stage; const apiVersion='2018-11-29'; - let API=null; - if (!process.env.IS_OFFLINE) { - API = AWS; - } else { - API = require('serverless-offline').AWS; - } - return new API.ApiGatewayManagementApi({ apiVersion, endpoint }); + return new AWS.ApiGatewayManagementApi({ apiVersion, endpoint }); }; const sendToClient = (data, connectionId, apigwManagementApi) => { diff --git a/manual_test_websocket/package-lock.json b/manual_test_websocket/package-lock.json index 9239a96c1..f732d5e1c 100644 --- a/manual_test_websocket/package-lock.json +++ b/manual_test_websocket/package-lock.json @@ -81,9 +81,9 @@ "dev": true }, "aws-sdk": { - "version": "2.464.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.464.0.tgz", - "integrity": "sha512-DsJ/V/Eaazio5klO49IXIgnDpBcObgOunFs0KsUbdPz5yxvznZQiez9LqTcaj0SaCS7zsrT9K5p+Jtbt60z1SA==", + "version": "2.465.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.465.0.tgz", + "integrity": "sha512-eS3g80QUbhOo0Rd/WTudtlc4cuNpLget6Re1KyDod6319QvW2il1q28VyvZK0/Yiu8GyVh5xGbThaLEQem+fLQ==", "requires": { "buffer": "4.9.1", "events": "1.1.1", diff --git a/manual_test_websocket/package.json b/manual_test_websocket/package.json index 07ee591bf..6cbea7cd4 100644 --- a/manual_test_websocket/package.json +++ b/manual_test_websocket/package.json @@ -13,7 +13,7 @@ "author": "", "license": "MIT", "dependencies": { - "aws-sdk": "^2.464.0" + "aws-sdk": "^2.465.0" }, "devDependencies": { "aws4": "^1.8.0", diff --git a/manual_test_websocket/test/e2e/ws.e2e.js b/manual_test_websocket/test/e2e/ws.e2e.js index 4c5c787e7..c9740ea41 100644 --- a/manual_test_websocket/test/e2e/ws.e2e.js +++ b/manual_test_websocket/test/e2e/ws.e2e.js @@ -150,7 +150,7 @@ describe('serverless', ()=>{ expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c2.id, queryStringParameters:{now, before:'123456789'}}}); }).timeout(timeout); - it('should be able to receive messages to via REST API', async ()=>{ + it('should be able to receive messages via REST API', async ()=>{ const c1=await createClient(); const c2=await createClient(); const url=new URL(endpoint); diff --git a/src/index.js b/src/index.js index c2de2f0f0..042f833b7 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,6 @@ const fs = require('fs'); const path = require('path'); const { exec } = require('child_process'); -const http = require('http'); // External dependencies const Hapi = require('hapi'); @@ -166,47 +165,6 @@ class Offline { this.serverlessLog('https://github.com/dherault/serverless-offline/issues'); } - static get AWS() { - return { - ApiGatewayManagementApi:class { - // constructor(apiVersion, client) { - // } - - postToConnection({ ConnectionId, Data }) { - return { - promise:() => { - const p = new Promise((resolve, reject) => { - const options = { - hostname: 'localhost', - port: 3001, - path: `/@connections/${ConnectionId}`, - method: 'POST', - headers: { - 'Content-Type': 'text/plain', - 'Content-Length': Buffer.byteLength(Data), - }, - }; - - const req = http.request(options, res => { - if (res.statusCode === 200) resolve(); else reject(); - }); - - req.on('error', () => { - reject(); - }); - - req.write(Data); - req.end(); - }); - - return p; - }, - }; - } - }, - }; - } - // Entry point for the plugin (sls offline) when running 'sls offline start' start() { this._checkVersion(); @@ -461,6 +419,44 @@ class Offline { if (ws.readyState === /* OPEN */1) ws.send(JSON.stringify({ message:'Internal server error', connectionId, requestId:'1234567890' })); }); }; + + const createEvent = (eventType, connectionId, payload) => { + const event = { + requestContext: { + routeKey: '$default', + messageId: 'acZVYfX5IAMCJWA=', + eventType, + extendedRequestId: 'acZVYFNUIAMFneQ=', + requestTime: '29/May/2019:11:39:01 +0000', + messageDirection: 'IN', + stage: 'dev', + connectedAt: 1559129941471, + requestTimeEpoch: 1559129941696, + identity: + { cognitoIdentityPoolId: null, + cognitoIdentityId: null, + principalOrgId: null, + cognitoAuthenticationType: null, + userArn: null, + userAgent: null, + accountId: null, + caller: null, + sourceIp: '46.116.162.112', + accessKey: null, + cognitoAuthenticationProvider: null, + user: null }, + requestId: 'acZVYFNUIAMFneQ=', + domainName: 'localhost', + connectionId, + apiId: 'private', + }, + body: JSON.stringify(payload), + isBase64Encoded: false, + apiGatewayUrl: `http${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port + 1}`, + }; + + return event; + }; this.wsServer.route({ method: 'POST', @@ -487,16 +483,16 @@ class Offline { const queryStringParameters = parseQuery(req.url); const connectionId = utils.randomId(); this.clients.set(ws, connectionId); - let params = { requestContext: { eventType:'CONNECT', connectionId } }; - if (Object.keys(queryStringParameters).length > 0) params = { queryStringParameters, ...params }; + let event = createEvent('CONNECT', connectionId); + if (Object.keys(queryStringParameters).length > 0) event = { queryStringParameters, ...event }; - doAction(ws, connectionId, '$connect', params); + doAction(ws, connectionId, '$connect', event); }, disconnect: ({ ws }) => { const connectionId = this.clients.get(ws); this.clients.delete(ws); - - doAction(ws, connectionId, '$disconnect', { requestContext: { eventType:'DISCONNECT', connectionId } }); + const event = createEvent('DISCONNECT', connectionId); + doAction(ws, connectionId, '$disconnect', event); }, }, }, @@ -505,7 +501,9 @@ class Offline { const { initially, ws } = request.websocket(); if (!request.payload || initially) return; const connectionId = this.clients.get(ws); - doAction(ws, connectionId, request.payload.action || '$default', { body:JSON.stringify(request.payload), requestContext: { domainName:'localhost', stage:'local', connectionId } }, true); + const action = request.payload.action || '$default'; + const event = createEvent('MESSAGE', connectionId, request.payload); + doAction(ws, connectionId, action, event, true); }, }); this.wsServer.route({ @@ -520,6 +518,7 @@ class Offline { this.wsServer.route({ method: 'POST', path: '/@connections/{connectionId}', + config: { payload: { parse: false } }, handler: (request, reply) => { const getByValue = (map, searchValue) => { for (const [key, value] of map.entries()) { @@ -538,7 +537,7 @@ class Offline { return; } if (!request.payload) return; - ws.send(request.payload); + ws.send(request.payload.toString()); response.send(); }, }); From f9600f870553c4317823e8913b748780e84dc151 Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Thu, 30 May 2019 08:52:05 +0300 Subject: [PATCH 16/71] Last commit leftovers --- manual_test_websocket/handler.js | 1 - 1 file changed, 1 deletion(-) diff --git a/manual_test_websocket/handler.js b/manual_test_websocket/handler.js index 263bd8334..9704167b7 100644 --- a/manual_test_websocket/handler.js +++ b/manual_test_websocket/handler.js @@ -93,7 +93,6 @@ module.exports.deleteListener = async (event, context) => { }; const newAWSApiGatewayManagementApi=(event, context)=>{ - const endpoint=event.requestContext.domainName+'/'+event.requestContext.stage; let endpoint=event.apiGatewayUrl; if (!endpoint) endpoint = event.requestContext.domainName+'/'+event.requestContext.stage; const apiVersion='2018-11-29'; From f3cf815e82eebdc9d1ebaa2dc3cf0899f6e76296 Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Sat, 1 Jun 2019 19:31:58 +0300 Subject: [PATCH 17/71] Added context and event when calling the handler. --- manual_test_websocket/handler.js | 5 ++ manual_test_websocket/package-lock.json | 12 ++- manual_test_websocket/package.json | 3 +- manual_test_websocket/scripts/serverless..yml | 5 ++ manual_test_websocket/test/e2e/ws.e2e.js | 65 +++++++++++++++- src/index.js | 76 ++++++++++++------- 6 files changed, 132 insertions(+), 34 deletions(-) diff --git a/manual_test_websocket/handler.js b/manual_test_websocket/handler.js index 9704167b7..7538fe6c6 100644 --- a/manual_test_websocket/handler.js +++ b/manual_test_websocket/handler.js @@ -51,6 +51,11 @@ module.exports.getClientInfo = async (event, context) => { return successfullResponse; }; +module.exports.getCallInfo = async (event, context) => { + await sendToClient({action:'update', event:'call-info', info:{event:{...event, apiGatewayUrl:`${event.apiGatewayUrl}`}, context}}, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err=>console.log(err)); + return successfullResponse; +}; + module.exports.makeError = async (event, context) => { const obj=null; obj.non.non=1; diff --git a/manual_test_websocket/package-lock.json b/manual_test_websocket/package-lock.json index f732d5e1c..cd725033b 100644 --- a/manual_test_websocket/package-lock.json +++ b/manual_test_websocket/package-lock.json @@ -81,9 +81,9 @@ "dev": true }, "aws-sdk": { - "version": "2.465.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.465.0.tgz", - "integrity": "sha512-eS3g80QUbhOo0Rd/WTudtlc4cuNpLget6Re1KyDod6319QvW2il1q28VyvZK0/Yiu8GyVh5xGbThaLEQem+fLQ==", + "version": "2.466.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.466.0.tgz", + "integrity": "sha512-dWFpz774ONjP1Cb19VkLOfQSVTu5p5/uncZGovAe71NOfPGDSvrQKXOsKcuI1/k4oJyKW9z/GATF8ht8DkDWGg==", "requires": { "buffer": "4.9.1", "events": "1.1.1", @@ -833,6 +833,12 @@ "yargs-unparser": "1.5.0" } }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==", + "dev": true + }, "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", diff --git a/manual_test_websocket/package.json b/manual_test_websocket/package.json index 6cbea7cd4..ca19f52c7 100644 --- a/manual_test_websocket/package.json +++ b/manual_test_websocket/package.json @@ -13,7 +13,7 @@ "author": "", "license": "MIT", "dependencies": { - "aws-sdk": "^2.465.0" + "aws-sdk": "^2.466.0" }, "devDependencies": { "aws4": "^1.8.0", @@ -21,6 +21,7 @@ "chai": "^4.2.0", "chai-http": "^4.3.0", "mocha": "^6.1.4", + "moment": "^2.24.0", "serverless-dynamodb-local": "^0.2.37", "ws": "^6.2.1" } diff --git a/manual_test_websocket/scripts/serverless..yml b/manual_test_websocket/scripts/serverless..yml index 11339bd57..62809f21d 100644 --- a/manual_test_websocket/scripts/serverless..yml +++ b/manual_test_websocket/scripts/serverless..yml @@ -61,6 +61,11 @@ functions: events: - websocket: route: getClientInfo + getCallInfo: + handler: handler.getCallInfo + events: + - websocket: + route: getCallInfo makeError: handler: handler.makeError events: diff --git a/manual_test_websocket/test/e2e/ws.e2e.js b/manual_test_websocket/test/e2e/ws.e2e.js index c9740ea41..54f448217 100644 --- a/manual_test_websocket/test/e2e/ws.e2e.js +++ b/manual_test_websocket/test/e2e/ws.e2e.js @@ -4,9 +4,9 @@ chai.use(chaiHttp); const expect = chai.expect; const aws4 = require('aws4'); const awscred = require('awscred'); +const moment = require('moment'); const endpoint = process.env.npm_config_endpoint||'ws://localhost:3001'; -const timeout = process.env.npm_config_timeout||1000; - +const timeout = process.env.npm_config_timeout?parseInt(process.env.npm_config_timeout):1000; const WebSocketTester=require('../support/WebSocketTester'); describe('serverless', ()=>{ @@ -32,7 +32,7 @@ describe('serverless', ()=>{ // req=chai.request('http://localhost:3001/dev').keepOpen(); cred=await new Promise((resolve, reject)=>{ awscred.loadCredentials(function(err, data) { if (err) reject(err); else resolve(data); }); - }); + }); }); beforeEach(()=>{ @@ -72,6 +72,65 @@ describe('serverless', ()=>{ expect(clientInfo).to.deep.equal({action:'update', event:'client-info', info:{id:clientInfo.info.id}}); }).timeout(timeout); + it('should receive correct call info', async ()=>{ + const c=await createClient(); + c.ws.send(JSON.stringify({action:'getCallInfo'})); + const callInfo=JSON.parse(await c.ws.receive1()); + const now=Date.now(); const url=new URL(endpoint); + + expect(callInfo).to.deep.equal({action:'update', event:'call-info', info:{ + event:{ + apiGatewayUrl: `${callInfo.info.event.apiGatewayUrl}`, + body: '{\"action\":\"getCallInfo\"}', + isBase64Encoded: false, + requestContext: { + apiId: callInfo.info.event.requestContext.apiId, + connectedAt:callInfo.info.event.requestContext.connectedAt, + connectionId: `${c.id}`, + domainName: url.hostname, + eventType: 'MESSAGE', + extendedRequestId: callInfo.info.event.requestContext.extendedRequestId, + identity: { + accessKey: null, + accountId: null, + caller: null, + cognitoAuthenticationProvider: null, + cognitoAuthenticationType: null, + cognitoIdentityId: null, + cognitoIdentityPoolId: null, + principalOrgId: null, + sourceIp: callInfo.info.event.requestContext.identity.sourceIp, + user: null, + userAgent: null, + userArn: null, + }, + messageDirection: 'IN', + messageId: callInfo.info.event.requestContext.messageId, + requestId: callInfo.info.event.requestContext.requestId, + requestTime: callInfo.info.event.requestContext.requestTime, + requestTimeEpoch: callInfo.info.event.requestContext.requestTimeEpoch, + routeKey: 'getCallInfo', + stage: callInfo.info.event.requestContext.stage, + }, + }, + context:{ + awsRequestId: callInfo.info.context.awsRequestId, + callbackWaitsForEmptyEventLoop: true, + functionName: callInfo.info.context.functionName, + functionVersion: '$LATEST', + invokedFunctionArn: callInfo.info.context.invokedFunctionArn, + invokeid: callInfo.info.context.invokeid, + logGroupName: callInfo.info.context.logGroupName, + logStreamName: callInfo.info.context.logStreamName, + memoryLimitInMB: callInfo.info.context.memoryLimitInMB, + }}}); + expect(callInfo.info.event.requestContext.connectedAt).to.be.lt(callInfo.info.event.requestContext.requestTimeEpoch); + expect(callInfo.info.event.requestContext.connectedAt).to.be.within(now-timeout, now+timeout); + expect(callInfo.info.event.requestContext.requestTimeEpoch).to.be.within(now-timeout, now+timeout); + expect(moment.utc(callInfo.info.event.requestContext.requestTime, 'D/MMM/YYYY:H:m:s Z').toDate().getTime()).to.be.within(now-timeout, now+timeout); + if (endpoint.startsWith('ws://locahost')) expect(callInfo.info.event.apiGatewayUrl).to.equal(endpoint.replace('ws://', 'http://').replace('wss://', 'https://')); + }).timeout(timeout); + it('should call default handler when no such action exists', async ()=>{ const ws=await createWebSocket(); const payload=JSON.stringify({action:'action'+Date.now()}); diff --git a/src/index.js b/src/index.js index 042f833b7..f3aa3bb1c 100644 --- a/src/index.js +++ b/src/index.js @@ -411,27 +411,29 @@ class Offline { this.wsServer.ext('onPreResponse', corsHeaders); this.wsServer.register(require('hapi-plugin-websocket'), err => err && this.serverlessLog(err)); - const doAction = (ws, connectionId, name, event, doDeafultAction/* , onError */) => { + const doAction = (ws, connectionId, name, event, context, doDeafultAction/* , onError */) => { let action = this.wsActions[name]; if (!action && doDeafultAction) action = this.wsActions.$default; if (!action) return; - action.handler(event, {}, () => {}).catch(() => { + action.handler(event, context, () => {}).catch(() => { if (ws.readyState === /* OPEN */1) ws.send(JSON.stringify({ message:'Internal server error', connectionId, requestId:'1234567890' })); }); }; - const createEvent = (eventType, connectionId, payload) => { + const createEvent = (action, eventType, connection, payload) => { + const now = new Date(); + const months=['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; const event = { requestContext: { - routeKey: '$default', - messageId: 'acZVYfX5IAMCJWA=', + routeKey: action, + messageId: `${utils.randomId()}`, eventType, - extendedRequestId: 'acZVYFNUIAMFneQ=', - requestTime: '29/May/2019:11:39:01 +0000', + extendedRequestId: `${utils.randomId()}`, + requestTime: `${now.getUTCDate()}/${months[now.getUTCMonth()]}/${now.getUTCFullYear()}:${now.getUTCHours()}:${now.getUTCMinutes()}:${now.getSeconds()} +0000`,//'29/May/2019:11:39:01 +0000', messageDirection: 'IN', - stage: 'dev', - connectedAt: 1559129941471, - requestTimeEpoch: 1559129941696, + stage: 'local', + connectedAt: connection.connectionTime, + requestTimeEpoch: now.getTime(), identity: { cognitoIdentityPoolId: null, cognitoIdentityId: null, @@ -441,13 +443,13 @@ class Offline { userAgent: null, accountId: null, caller: null, - sourceIp: '46.116.162.112', + sourceIp: '127.0.0.1', accessKey: null, cognitoAuthenticationProvider: null, user: null }, - requestId: 'acZVYFNUIAMFneQ=', + requestId: `${utils.randomId()}`, domainName: 'localhost', - connectionId, + connectionId:connection.connectionId, apiId: 'private', }, body: JSON.stringify(payload), @@ -457,6 +459,22 @@ class Offline { return event; }; + + const createContext = (action) => { + const context={ + awsRequestId: `offline_awsRequestId_for_${action}`, + callbackWaitsForEmptyEventLoop: true, + functionName: action, + functionVersion: '$LATEST', + invokedFunctionArn: `offline_invokedFunctionArn_for_${action}`, + invokeid: `offline_invokeid_for_${action}`, + logGroupName: `offline_logGroupName_for_${action}`, + logStreamName: `offline_logStreamName_for_${action}`, + memoryLimitInMB: '1024', + }; + + return context; + }; this.wsServer.route({ method: 'POST', @@ -481,18 +499,21 @@ class Offline { }; const queryStringParameters = parseQuery(req.url); - const connectionId = utils.randomId(); - this.clients.set(ws, connectionId); - let event = createEvent('CONNECT', connectionId); + const connection = { connectionId:utils.randomId(), connectionTime:Date.now() }; + this.clients.set(ws, connection); + let event = createEvent('$connect', 'CONNECT', connection); if (Object.keys(queryStringParameters).length > 0) event = { queryStringParameters, ...event }; + const context = createContext('$connect'); - doAction(ws, connectionId, '$connect', event); + doAction(ws, connection.connectionId, '$connect', event, context); }, disconnect: ({ ws }) => { - const connectionId = this.clients.get(ws); + const connection = this.clients.get(ws); this.clients.delete(ws); - const event = createEvent('DISCONNECT', connectionId); - doAction(ws, connectionId, '$disconnect', event); + const event = createEvent('$disconnect', 'DISCONNECT', connection); + const context = createContext('$disconnect'); + + doAction(ws, connection.connectionId, '$disconnect', event, context); }, }, }, @@ -500,10 +521,11 @@ class Offline { handler: request /* , reply */ => { const { initially, ws } = request.websocket(); if (!request.payload || initially) return; - const connectionId = this.clients.get(ws); + const connection = this.clients.get(ws); const action = request.payload.action || '$default'; - const event = createEvent('MESSAGE', connectionId, request.payload); - doAction(ws, connectionId, action, event, true); + const event = createEvent(action, 'MESSAGE', connection, request.payload); + const context = createContext(action); + doAction(ws, connection.connectionId, action, event, context, true); }, }); this.wsServer.route({ @@ -520,16 +542,16 @@ class Offline { path: '/@connections/{connectionId}', config: { payload: { parse: false } }, handler: (request, reply) => { - const getByValue = (map, searchValue) => { - for (const [key, value] of map.entries()) { - if (value === searchValue) return key; + const getByConnectionId = (map, searchValue) => { + for (const [key, connection] of map.entries()) { + if (connection.connectionId === searchValue) return key; } return undefined; }; const response = reply.response().hold(); - const ws = getByValue(this.clients, request.params.connectionId); + const ws = getByConnectionId(this.clients, request.params.connectionId); if (!ws) { response.statusCode = 410; response.send(); From 4521b886be75897eaae13ffd1bf00b9ab02b408a Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Mon, 3 Jun 2019 14:08:40 +0300 Subject: [PATCH 18/71] Added context and event for connect and disconnect --- manual_test_websocket/handler.js | 4 +- manual_test_websocket/test/e2e/ws.e2e.js | 219 ++++++++++++++++------- src/index.js | 123 +++++++++---- 3 files changed, 240 insertions(+), 106 deletions(-) diff --git a/manual_test_websocket/handler.js b/manual_test_websocket/handler.js index 7538fe6c6..c19236d9f 100644 --- a/manual_test_websocket/handler.js +++ b/manual_test_websocket/handler.js @@ -27,7 +27,7 @@ module.exports.connect = async (event, context) => { if (listener.Item) { const timeout=new Promise((resolve) => setTimeout(resolve,100)); const send=sendToClient( // sendToClient won't return on AWS when client doesn't exits so we set a timeout - JSON.stringify({action:'update', event:'connect', info:{id:event.requestContext.connectionId, queryStringParameters:event.queryStringParameters}}), + JSON.stringify({action:'update', event:'connect', info:{id:event.requestContext.connectionId, event:{...event, apiGatewayUrl:`${event.apiGatewayUrl}`}, context}}), listener.Item.id, newAWSApiGatewayManagementApi(event, context)).catch(()=>{}); await Promise.race([send, timeout]); @@ -37,7 +37,7 @@ module.exports.connect = async (event, context) => { module.exports.disconnect = async (event, context) => { const listener=await ddb.get({TableName:'listeners', Key:{name:'default'}}).promise(); - if (listener.Item) await sendToClient(JSON.stringify({action:'update', event:'disconnect', info:{id:event.requestContext.connectionId}}), listener.Item.id, newAWSApiGatewayManagementApi(event, context)).catch(()=>{}); + if (listener.Item) await sendToClient(JSON.stringify({action:'update', event:'disconnect', info:{id:event.requestContext.connectionId, event:{...event, apiGatewayUrl:`${event.apiGatewayUrl}`}, context}}), listener.Item.id, newAWSApiGatewayManagementApi(event, context)).catch(()=>{}); return successfullResponse; }; diff --git a/manual_test_websocket/test/e2e/ws.e2e.js b/manual_test_websocket/test/e2e/ws.e2e.js index 54f448217..1ef3aa91c 100644 --- a/manual_test_websocket/test/e2e/ws.e2e.js +++ b/manual_test_websocket/test/e2e/ws.e2e.js @@ -72,65 +72,6 @@ describe('serverless', ()=>{ expect(clientInfo).to.deep.equal({action:'update', event:'client-info', info:{id:clientInfo.info.id}}); }).timeout(timeout); - it('should receive correct call info', async ()=>{ - const c=await createClient(); - c.ws.send(JSON.stringify({action:'getCallInfo'})); - const callInfo=JSON.parse(await c.ws.receive1()); - const now=Date.now(); const url=new URL(endpoint); - - expect(callInfo).to.deep.equal({action:'update', event:'call-info', info:{ - event:{ - apiGatewayUrl: `${callInfo.info.event.apiGatewayUrl}`, - body: '{\"action\":\"getCallInfo\"}', - isBase64Encoded: false, - requestContext: { - apiId: callInfo.info.event.requestContext.apiId, - connectedAt:callInfo.info.event.requestContext.connectedAt, - connectionId: `${c.id}`, - domainName: url.hostname, - eventType: 'MESSAGE', - extendedRequestId: callInfo.info.event.requestContext.extendedRequestId, - identity: { - accessKey: null, - accountId: null, - caller: null, - cognitoAuthenticationProvider: null, - cognitoAuthenticationType: null, - cognitoIdentityId: null, - cognitoIdentityPoolId: null, - principalOrgId: null, - sourceIp: callInfo.info.event.requestContext.identity.sourceIp, - user: null, - userAgent: null, - userArn: null, - }, - messageDirection: 'IN', - messageId: callInfo.info.event.requestContext.messageId, - requestId: callInfo.info.event.requestContext.requestId, - requestTime: callInfo.info.event.requestContext.requestTime, - requestTimeEpoch: callInfo.info.event.requestContext.requestTimeEpoch, - routeKey: 'getCallInfo', - stage: callInfo.info.event.requestContext.stage, - }, - }, - context:{ - awsRequestId: callInfo.info.context.awsRequestId, - callbackWaitsForEmptyEventLoop: true, - functionName: callInfo.info.context.functionName, - functionVersion: '$LATEST', - invokedFunctionArn: callInfo.info.context.invokedFunctionArn, - invokeid: callInfo.info.context.invokeid, - logGroupName: callInfo.info.context.logGroupName, - logStreamName: callInfo.info.context.logStreamName, - memoryLimitInMB: callInfo.info.context.memoryLimitInMB, - }}}); - expect(callInfo.info.event.requestContext.connectedAt).to.be.lt(callInfo.info.event.requestContext.requestTimeEpoch); - expect(callInfo.info.event.requestContext.connectedAt).to.be.within(now-timeout, now+timeout); - expect(callInfo.info.event.requestContext.requestTimeEpoch).to.be.within(now-timeout, now+timeout); - expect(moment.utc(callInfo.info.event.requestContext.requestTime, 'D/MMM/YYYY:H:m:s Z').toDate().getTime()).to.be.within(now-timeout, now+timeout); - if (endpoint.startsWith('ws://locahost')) expect(callInfo.info.event.apiGatewayUrl).to.equal(endpoint.replace('ws://', 'http://').replace('wss://', 'https://')); - }).timeout(timeout); - it('should call default handler when no such action exists', async ()=>{ const ws=await createWebSocket(); const payload=JSON.stringify({action:'action'+Date.now()}); @@ -179,22 +120,166 @@ describe('serverless', ()=>{ await ws.receive1(); const c1=await createClient(); - expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c1.id}}); + const connect1 = JSON.parse(await ws.receive1()); delete connect1.info.event; delete delete connect1.info.context; + expect(connect1).to.deep.equal({action:'update', event:'connect', info:{id:c1.id}}); const c2=await createClient(); - expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c2.id}}); + const connect2 = JSON.parse(await ws.receive1()); delete connect2.info.event; delete delete connect2.info.context; + expect(connect2).to.deep.equal({action:'update', event:'connect', info:{id:c2.id}}); c2.ws.close(); - expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'disconnect', info:{id:c2.id}}); + const disconnect2 = JSON.parse(await ws.receive1()); delete disconnect2.info.event; delete delete disconnect2.info.context; + expect(disconnect2).to.deep.equal({action:'update', event:'disconnect', info:{id:c2.id}}); const c3=await createClient(); - expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c3.id}}); + const connect3 = JSON.parse(await ws.receive1()); delete connect3.info.event; delete delete connect3.info.context; + expect(connect3).to.deep.equal({action:'update', event:'connect', info:{id:c3.id}}); c1.ws.close(); - expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'disconnect', info:{id:c1.id}}); + const disconnect1 = JSON.parse(await ws.receive1()); delete disconnect1.info.event; delete delete disconnect1.info.context; + expect(disconnect1).to.deep.equal({action:'update', event:'disconnect', info:{id:c1.id}}); c3.ws.close(); - expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'disconnect', info:{id:c3.id}}); + const disconnect3 = JSON.parse(await ws.receive1()); delete disconnect3.info.event; delete delete disconnect3.info.context; + expect(disconnect3).to.deep.equal({action:'update', event:'disconnect', info:{id:c3.id}}); + }).timeout(timeout); + + const createExpectedEvent=(connectionId, action, eventType, actualEvent)=>{ + const url=new URL(endpoint); + const expected={ + apiGatewayUrl: `${actualEvent.apiGatewayUrl}`, + isBase64Encoded: false, + requestContext: { + apiId: actualEvent.requestContext.apiId, + connectedAt: actualEvent.requestContext.connectedAt, + connectionId: `${connectionId}`, + domainName: url.hostname, + eventType, + extendedRequestId: actualEvent.requestContext.extendedRequestId, + identity: { + accessKey: null, + accountId: null, + caller: null, + cognitoAuthenticationProvider: null, + cognitoAuthenticationType: null, + cognitoIdentityId: null, + cognitoIdentityPoolId: null, + principalOrgId: null, + sourceIp: actualEvent.requestContext.identity.sourceIp, + user: null, + userAgent: null, + userArn: null, + }, + messageDirection: 'IN', + messageId: actualEvent.requestContext.messageId, + requestId: actualEvent.requestContext.requestId, + requestTime: actualEvent.requestContext.requestTime, + requestTimeEpoch: actualEvent.requestContext.requestTimeEpoch, + routeKey: action, + stage: actualEvent.requestContext.stage, + }, + }; + + return expected; + }; + + const createExpectedContext=(actualContext)=>{ + const expected={ + awsRequestId: actualContext.awsRequestId, + callbackWaitsForEmptyEventLoop: true, + functionName: actualContext.functionName, + functionVersion: '$LATEST', + invokedFunctionArn: actualContext.invokedFunctionArn, + invokeid: actualContext.invokeid, + logGroupName: actualContext.logGroupName, + logStreamName: actualContext.logStreamName, + memoryLimitInMB: actualContext.memoryLimitInMB, + }; + + return expected; + }; + + const createExpectedConnectHeaders=(actualHeaders)=>{ + const url=new URL(endpoint); + const expected={ + Host: url.hostname, + 'Sec-WebSocket-Extensions': actualHeaders['Sec-WebSocket-Extensions'], + 'Sec-WebSocket-Key': actualHeaders['Sec-WebSocket-Key'], + 'Sec-WebSocket-Version': actualHeaders['Sec-WebSocket-Version'], + 'X-Amzn-Trace-Id': actualHeaders['X-Amzn-Trace-Id'], + 'X-Forwarded-For': actualHeaders['X-Forwarded-For'], + 'X-Forwarded-Port': `${url.port||443}`, + 'X-Forwarded-Proto': `${url.protocol.replace('ws', 'http').replace('wss', 'https').replace(':', '')}` + }; + + return expected; + }; + + const createExpectedDisconnectHeaders=(actualHeaders)=>{ + const url=new URL(endpoint); + const expected={ + Host: url.hostname, + 'x-api-key': '', + 'x-restapi': '', + }; + + return expected; + }; + + const createExpectedConnectMultiValueHeaders=(actualHeaders)=>{ + const expected=createExpectedConnectHeaders(actualHeaders); + Object.keys(expected).map((key, index)=>{ + expected[key] = [expected[key]]; + }); + return expected; + }; + + const createExpectedDisconnectMultiValueHeaders=(actualHeaders)=>{ + const expected=createExpectedDisconnectHeaders(actualHeaders); + Object.keys(expected).map((key, index)=>{ + expected[key] = [expected[key]]; + }); + return expected; + }; + + it('should receive correct call info', async ()=>{ + const ws=await createWebSocket(); + await ws.send(JSON.stringify({action:'registerListener'})); + await ws.receive1(); + + // connect + const c=await createClient(); + const connect=JSON.parse(await ws.receive1()); + let now=Date.now(); + let expectedCallInfo={id:c.id, event:{headers:createExpectedConnectHeaders(connect.info.event.headers), multiValueHeaders:createExpectedConnectMultiValueHeaders(connect.info.event.headers), ...createExpectedEvent(c.id, '$connect', 'CONNECT', connect.info.event)}, context:createExpectedContext(connect.info.context)}; + expect(connect).to.deep.equal({action:'update', event:'connect', info:expectedCallInfo}); + expect(connect.info.event.requestContext.requestTimeEpoch).to.be.within(connect.info.event.requestContext.connectedAt-10, connect.info.event.requestContext.requestTimeEpoch+10); + expect(connect.info.event.requestContext.connectedAt).to.be.within(now-timeout, now); + expect(connect.info.event.requestContext.requestTimeEpoch).to.be.within(now-timeout, now); + expect(moment.utc(connect.info.event.requestContext.requestTime, 'D/MMM/YYYY:H:m:s Z').toDate().getTime()).to.be.within(now-timeout, now); + if (endpoint.startsWith('ws://locahost')) { + expect(connect.info.event.apiGatewayUrl).to.equal(endpoint.replace('ws://', 'http://').replace('wss://', 'https://')); + expect(connect.info.event.headers['X-Forwarded-For']).to.be.equal('127.0.0.1'); + } + + // getCallInfo + c.ws.send(JSON.stringify({action:'getCallInfo'})); + const callInfo=JSON.parse(await c.ws.receive1()); + now=Date.now(); + expectedCallInfo={event:{body: '{\"action\":\"getCallInfo\"}', ...createExpectedEvent(c.id, 'getCallInfo', 'MESSAGE', callInfo.info.event)}, context:createExpectedContext(callInfo.info.context)}; + expect(callInfo).to.deep.equal({action:'update', event:'call-info', info:expectedCallInfo}); + expect(callInfo.info.event.requestContext.connectedAt).to.be.lt(callInfo.info.event.requestContext.requestTimeEpoch); + expect(callInfo.info.event.requestContext.connectedAt).to.be.within(now-timeout, now); + expect(callInfo.info.event.requestContext.requestTimeEpoch).to.be.within(now-timeout, now); + expect(moment.utc(callInfo.info.event.requestContext.requestTime, 'D/MMM/YYYY:H:m:s Z').toDate().getTime()).to.be.within(now-timeout, now); + if (endpoint.startsWith('ws://locahost')) expect(callInfo.info.event.apiGatewayUrl).to.equal(endpoint.replace('ws://', 'http://').replace('wss://', 'https://')); + + // disconnect + c.ws.close(); + const disconnect=JSON.parse(await ws.receive1()); + now=Date.now(); + expectedCallInfo={id:c.id, event:{headers:createExpectedDisconnectHeaders(disconnect.info.event.headers), multiValueHeaders:createExpectedDisconnectMultiValueHeaders(disconnect.info.event.headers), ...createExpectedEvent(c.id, '$disconnect', 'DISCONNECT', disconnect.info.event)}, context:createExpectedContext(disconnect.info.context)}; + expect(disconnect).to.deep.equal({action:'update', event:'disconnect', info:expectedCallInfo}); }).timeout(timeout); it('should be able to parse query string', async ()=>{ @@ -205,8 +290,8 @@ describe('serverless', ()=>{ const c1=await createClient(); const c2=await createClient(`now=${now}&before=123456789`); - expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c1.id}}); - expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c2.id, queryStringParameters:{now, before:'123456789'}}}); + expect(JSON.parse(await ws.receive1()).info.event.queryStringParameters).to.be.undefined; + expect(JSON.parse(await ws.receive1()).info.event.queryStringParameters).to.deep.equal({now, before:'123456789'}); }).timeout(timeout); it('should be able to receive messages via REST API', async ()=>{ diff --git a/src/index.js b/src/index.js index f3aa3bb1c..cf3f433b8 100644 --- a/src/index.js +++ b/src/index.js @@ -420,38 +420,44 @@ class Offline { }); }; - const createEvent = (action, eventType, connection, payload) => { + const createRequestContext = (action, eventType, connection) => { const now = new Date(); - const months=['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + const requestContext = { + routeKey: action, + messageId: `${utils.randomId()}`, + eventType, + extendedRequestId: `${utils.randomId()}`, + requestTime: `${now.getUTCDate()}/${months[now.getUTCMonth()]}/${now.getUTCFullYear()}:${now.getUTCHours()}:${now.getUTCMinutes()}:${now.getSeconds()} +0000`, + messageDirection: 'IN', + stage: 'local', + connectedAt: connection.connectionTime, + requestTimeEpoch: now.getTime(), + identity: + { cognitoIdentityPoolId: null, + cognitoIdentityId: null, + principalOrgId: null, + cognitoAuthenticationType: null, + userArn: null, + userAgent: null, + accountId: null, + caller: null, + sourceIp: '127.0.0.1', + accessKey: null, + cognitoAuthenticationProvider: null, + user: null }, + requestId: `${utils.randomId()}`, + domainName: 'localhost', + connectionId:connection.connectionId, + apiId: 'private', + }; + + return requestContext; + }; + + const createEvent = (action, eventType, connection, payload) => { const event = { - requestContext: { - routeKey: action, - messageId: `${utils.randomId()}`, - eventType, - extendedRequestId: `${utils.randomId()}`, - requestTime: `${now.getUTCDate()}/${months[now.getUTCMonth()]}/${now.getUTCFullYear()}:${now.getUTCHours()}:${now.getUTCMinutes()}:${now.getSeconds()} +0000`,//'29/May/2019:11:39:01 +0000', - messageDirection: 'IN', - stage: 'local', - connectedAt: connection.connectionTime, - requestTimeEpoch: now.getTime(), - identity: - { cognitoIdentityPoolId: null, - cognitoIdentityId: null, - principalOrgId: null, - cognitoAuthenticationType: null, - userArn: null, - userAgent: null, - accountId: null, - caller: null, - sourceIp: '127.0.0.1', - accessKey: null, - cognitoAuthenticationProvider: null, - user: null }, - requestId: `${utils.randomId()}`, - domainName: 'localhost', - connectionId:connection.connectionId, - apiId: 'private', - }, + requestContext: createRequestContext(action, eventType, connection), body: JSON.stringify(payload), isBase64Encoded: false, apiGatewayUrl: `http${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port + 1}`, @@ -460,8 +466,51 @@ class Offline { return event; }; - const createContext = (action) => { - const context={ + const createConnectEvent = (action, eventType, connection) => { + const headers = { + Host: 'localhost', + 'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits', + 'Sec-WebSocket-Key': `${utils.randomId()}`, + 'Sec-WebSocket-Version': '13', + 'X-Amzn-Trace-Id': `Root=${utils.randomId()}`, + 'X-Forwarded-For': '127.0.0.1', + 'X-Forwarded-Port': `${this.options.port + 1}`, + 'X-Forwarded-Proto': `http${this.options.httpsProtocol ? 's' : ''}`, + }; + const multiValueHeaders = { ...headers }; + Object.keys(multiValueHeaders).map(key => multiValueHeaders[key] = [multiValueHeaders[key]]); + const event = { + headers, + multiValueHeaders, + requestContext: createRequestContext(action, eventType, connection), + apiGatewayUrl: `http${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port + 1}`, + isBase64Encoded: false, + }; + + return event; + }; + + const createDisconnectEvent = (action, eventType, connection) => { + const headers = { + Host: 'localhost', + 'x-api-key': '', + 'x-restapi': '', + }; + const multiValueHeaders = { ...headers }; + Object.keys(multiValueHeaders).map(key => multiValueHeaders[key] = [multiValueHeaders[key]]); + const event = { + headers, + multiValueHeaders, + requestContext: createRequestContext(action, eventType, connection), + apiGatewayUrl: `http${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port + 1}`, + isBase64Encoded: false, + }; + + return event; + }; + + const createContext = action => { + const context = { awsRequestId: `offline_awsRequestId_for_${action}`, callbackWaitsForEmptyEventLoop: true, functionName: action, @@ -501,17 +550,17 @@ class Offline { const queryStringParameters = parseQuery(req.url); const connection = { connectionId:utils.randomId(), connectionTime:Date.now() }; this.clients.set(ws, connection); - let event = createEvent('$connect', 'CONNECT', connection); + let event = createConnectEvent('$connect', 'CONNECT', connection); if (Object.keys(queryStringParameters).length > 0) event = { queryStringParameters, ...event }; - const context = createContext('$connect'); + const context = createContext('$connect'); doAction(ws, connection.connectionId, '$connect', event, context); }, disconnect: ({ ws }) => { const connection = this.clients.get(ws); this.clients.delete(ws); - const event = createEvent('$disconnect', 'DISCONNECT', connection); - const context = createContext('$disconnect'); + const event = createDisconnectEvent('$disconnect', 'DISCONNECT', connection); + const context = createContext('$disconnect'); doAction(ws, connection.connectionId, '$disconnect', event, context); }, @@ -524,7 +573,7 @@ class Offline { const connection = this.clients.get(ws); const action = request.payload.action || '$default'; const event = createEvent(action, 'MESSAGE', connection, request.payload); - const context = createContext(action); + const context = createContext(action); doAction(ws, connection.connectionId, action, event, context, true); }, }); From 1e559b76805701fc07a5efd051508d3ead161ba3 Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Wed, 5 Jun 2019 00:49:02 +0300 Subject: [PATCH 19/71] hapi@18 and hapi-plugin-websocket@2 support --- manual_test_websocket/handler.js | 4 + manual_test_websocket/package-lock.json | 8 +- manual_test_websocket/package.json | 2 +- package-lock.json | 146 +++++++++++++++++++++++- package.json | 1 + src/index.js | 126 +++++++++++--------- 6 files changed, 223 insertions(+), 64 deletions(-) diff --git a/manual_test_websocket/handler.js b/manual_test_websocket/handler.js index c19236d9f..8b75497e4 100644 --- a/manual_test_websocket/handler.js +++ b/manual_test_websocket/handler.js @@ -22,6 +22,7 @@ const errorResponse = { // }; module.exports.connect = async (event, context) => { + // console.log('connect:'); const listener=await ddb.get({TableName:'listeners', Key:{name:'default'}}).promise(); if (listener.Item) { @@ -47,6 +48,7 @@ module.exports.defaultHandler = async (event, context) => { }; module.exports.getClientInfo = async (event, context) => { + // console.log('getClientInfo:'); await sendToClient({action:'update', event:'client-info', info:{id:event.requestContext.connectionId}}, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err=>console.log(err)); return successfullResponse; }; @@ -99,12 +101,14 @@ module.exports.deleteListener = async (event, context) => { const newAWSApiGatewayManagementApi=(event, context)=>{ let endpoint=event.apiGatewayUrl; + if (!endpoint) endpoint = event.requestContext.domainName+'/'+event.requestContext.stage; const apiVersion='2018-11-29'; return new AWS.ApiGatewayManagementApi({ apiVersion, endpoint }); }; const sendToClient = (data, connectionId, apigwManagementApi) => { + // console.log(`sendToClient:${connectionId}`); let sendee=data; if ('object'==typeof data) sendee=JSON.stringify(data); diff --git a/manual_test_websocket/package-lock.json b/manual_test_websocket/package-lock.json index cd725033b..6373297df 100644 --- a/manual_test_websocket/package-lock.json +++ b/manual_test_websocket/package-lock.json @@ -1344,12 +1344,12 @@ "dev": true }, "ws": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", - "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.0.0.tgz", + "integrity": "sha512-cknCal4k0EAOrh1SHHPPWWh4qm93g1IuGGGwBjWkXmCG7LsDtL8w9w+YVfaF+KSVwiHQKDIMsSLBVftKf9d1pg==", "dev": true, "requires": { - "async-limiter": "~1.0.0" + "async-limiter": "^1.0.0" } }, "xml2js": { diff --git a/manual_test_websocket/package.json b/manual_test_websocket/package.json index ca19f52c7..13aa78a7a 100644 --- a/manual_test_websocket/package.json +++ b/manual_test_websocket/package.json @@ -23,6 +23,6 @@ "mocha": "^6.1.4", "moment": "^2.24.0", "serverless-dynamodb-local": "^0.2.37", - "ws": "^6.2.1" + "ws": "^7.0.0" } } diff --git a/package-lock.json b/package-lock.json index 3b7888204..a08dfc1ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "serverless-offline", - "version": "4.10.6", + "version": "5.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -571,6 +571,11 @@ "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", "dev": true }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, "axobject-query": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.2.tgz", @@ -756,6 +761,19 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "bignumber.js": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-8.1.1.tgz", + "integrity": "sha512-QD46ppGintwPGuL1KqmwhR0O+N2cZUg8JG/VzwI2e28sM9TqHjQB10lI4QAaMHVbLzwVLLAwEglpKPViWX+5NQ==" + }, + "boom": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-7.3.0.tgz", + "integrity": "sha512-Swpoyi2t5+GhOEGw8rEsKvTxFLIDiiKoUc2gsoV6Lyr43LHBIzch3k2MvYUs8RTROrIkVJ3Al0TkaOGjnb+B6A==", + "requires": { + "hoek": "6.x.x" + } + }, "boxen": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", @@ -904,6 +922,22 @@ "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==", "dev": true }, + "cbor": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-4.1.4.tgz", + "integrity": "sha512-SqNWyQnnYtKAPLA7lupvuGKrEgoF2rR/7I9rXdmW/9uxtmKdltthHTf8hfLLN1SIkoAFwz/jb6+VZuaHv3Lv6Q==", + "requires": { + "bignumber.js": "^8.0.1", + "commander": "^2.19.0", + "json-text-sequence": "^0.1", + "nofilter": "^1.0.1" + } + }, + "cbor-js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cbor-js/-/cbor-js-0.1.0.tgz", + "integrity": "sha1-yAzmEg84fo+qdDcN/aIdlluPx/k=" + }, "chai": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", @@ -1015,8 +1049,7 @@ "commander": { "version": "2.20.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", - "dev": true + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" }, "concat-map": { "version": "0.0.1", @@ -1148,6 +1181,11 @@ "object-keys": "^1.0.12" } }, + "delimit-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/delimit-stream/-/delimit-stream-0.1.0.tgz", + "integrity": "sha1-m4MZR3wOX4rrPONXrjBfwl6hzSs=" + }, "depcheck": { "version": "0.6.11", "resolved": "https://registry.npmjs.org/depcheck/-/depcheck-0.6.11.tgz", @@ -1400,6 +1438,17 @@ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", "dev": true }, + "encodr": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/encodr/-/encodr-1.2.0.tgz", + "integrity": "sha512-OHAfuXxoeXEeXFZ0Vu3CGegIVI1iuLLdVMy1EIVDBfvff1tMjVwRNBFuo5UbjBm3Efcu+GiIYGOt0H3NKDjPrw==", + "requires": { + "cbor": "4.1.4", + "cbor-js": "0.1.0", + "msgpack-lite": "0.1.26", + "utf8": "3.0.0" + } + }, "end-of-stream": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", @@ -1754,6 +1803,16 @@ "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", "dev": true }, + "event-lite": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.2.tgz", + "integrity": "sha512-HnSYx1BsJ87/p6swwzv+2v6B4X+uxUteoDfRxsAb1S1BePzQqOLevVmkdA15GHJVd9A9Ok6wygUR18Hu0YeV9g==" + }, + "eventemitter3": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", + "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==" + }, "execa": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", @@ -2038,6 +2097,18 @@ "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, + "hapi-plugin-websocket": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/hapi-plugin-websocket/-/hapi-plugin-websocket-2.0.16.tgz", + "integrity": "sha512-UE2JSrNIalXJmw4Qoi2FqhcguqTYQhjgPAVCtJUBhhZXD6IyA15guTdSeshBLxGfKcAFJmlpXs2LUMB25ARfOA==", + "requires": { + "boom": "7.3.0", + "hoek": "6.1.2", + "urijs": "1.19.1", + "websocket-framed": "1.2.0", + "ws": "6.1.2" + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -2101,6 +2172,11 @@ } } }, + "hoek": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.2.tgz", + "integrity": "sha512-6qhh/wahGYZHFSFw12tBbJw5fsAhhwrrG/y3Cs0YMTv2WzMnL0oLPnQJjv1QJvEfylRSOFuP+xCu+tdx0tD16Q==" + }, "homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -2125,6 +2201,11 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -2222,6 +2303,11 @@ } } }, + "int64-buffer": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/int64-buffer/-/int64-buffer-0.1.10.tgz", + "integrity": "sha1-J3siiofZWtd30HwTgyAiQGpHNCM=" + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -2379,8 +2465,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -2427,6 +2512,14 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, + "json-text-sequence": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/json-text-sequence/-/json-text-sequence-0.1.1.tgz", + "integrity": "sha1-py8hfcSvxGKf/1/rME3BvVGi89I=", + "requires": { + "delimit-stream": "0.1.0" + } + }, "jsonpath-plus": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-0.19.0.tgz", @@ -2951,6 +3044,17 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" }, + "msgpack-lite": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/msgpack-lite/-/msgpack-lite-0.1.26.tgz", + "integrity": "sha1-3TxQsm8FnyXn7e42REGDWOKprYk=", + "requires": { + "event-lite": "^0.1.1", + "ieee754": "^1.1.8", + "int64-buffer": "^0.1.9", + "isarray": "^1.0.0" + } + }, "mute-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", @@ -3009,6 +3113,11 @@ "semver": "^5.7.0" } }, + "nofilter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-1.0.2.tgz", + "integrity": "sha512-d38SORxm9UNoDsnPXajV9nBEebKX4/paXAlyRGnSjZuFbLLZDFUO4objr+tbybqsbqGXDWllb6gQoKUDc9q3Cg==" + }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -4266,6 +4375,11 @@ "punycode": "^2.1.0" } }, + "urijs": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.1.tgz", + "integrity": "sha512-xVrGVi94ueCJNrBSTjWqjvtgvl3cyOTThp2zaMaFNGp3F542TR6sM3f2o8RqZl+AwteClSVmoCyt0ka4RjQOQg==" + }, "url-parse-lax": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", @@ -4275,6 +4389,11 @@ "prepend-http": "^1.0.1" } }, + "utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", + "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==" + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -4296,6 +4415,15 @@ "integrity": "sha1-oW0CXrkxvQO1LzCMrtD0D86+lTI=", "dev": true }, + "websocket-framed": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/websocket-framed/-/websocket-framed-1.2.0.tgz", + "integrity": "sha512-rnVf9NisrPAKIzB0LLgLdnbiElRNZSeahKKXcicxzOxJdW4ZaCE7xR7nheBIoN2j++2fk6FD1GTg9d+rw/X9+g==", + "requires": { + "encodr": "1.2.0", + "eventemitter3": "3.1.0" + } + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -4418,6 +4546,14 @@ "signal-exit": "^3.0.2" } }, + "ws": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.2.tgz", + "integrity": "sha512-rfUqzvz0WxmSXtJpPMX2EeASXabOrSMk1ruMOV3JBTBjo4ac2lDjGGsbQSyxj8Odhw5fBib8ZKEjDNvgouNKYw==", + "requires": { + "async-limiter": "~1.0.0" + } + }, "xdg-basedir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", diff --git a/package.json b/package.json index c95394654..4ac5bfe30 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,7 @@ "@hapi/cryptiles": "^4.2.0", "@hapi/h2o2": "^8.3.0", "@hapi/hapi": "^18.3.1", + "hapi-plugin-websocket": "^2.0.16", "js-string-escape": "^1.0.1", "jsonpath-plus": "^0.19.0", "jsonschema": "^1.2.4", diff --git a/src/index.js b/src/index.js index aea61332d..de64f72bd 100755 --- a/src/index.js +++ b/src/index.js @@ -384,33 +384,26 @@ class Offline { } _createWebSocket() { - // Hapijs server creation - this.wsServer = new Hapi.Server({ - connections: { - router: { - stripTrailingSlash: !this.options.preserveTrailingSlash, // removes trailing slashes on incoming paths. - }, - }, - }); - - this.wsServer.register(h2o2, err => err && this.serverlessLog(err)); - - const connectionOptions = { + // start COPY PASTE FROM HTTP SERVER CODE + const serverOptions = { host: this.options.host, port: this.options.port + 1, + router: { + stripTrailingSlash: !this.options.preserveTrailingSlash, // removes trailing slashes on incoming paths. + }, }; const httpsDir = this.options.httpsProtocol; // HTTPS support if (typeof httpsDir === 'string' && httpsDir.length > 0) { - connectionOptions.tls = { + serverOptions.tls = { key: fs.readFileSync(path.resolve(httpsDir, 'key.pem'), 'ascii'), cert: fs.readFileSync(path.resolve(httpsDir, 'cert.pem'), 'ascii'), }; } - connectionOptions.state = this.options.enforceSecureCookies ? { + serverOptions.state = this.options.enforceSecureCookies ? { isHttpOnly: true, isSecure: true, isSameSite: false, @@ -420,18 +413,46 @@ class Offline { isSameSite: false, }; - // Passes the configuration object to the server - this.wsServer.connection(connectionOptions); + // Hapijs server creation + this.wsServer = hapi.server(serverOptions); + + this.wsServer.register(h2o2).catch(err => err && this.serverlessLog(err)); // Enable CORS preflight response - this.wsServer.ext('onPreResponse', corsHeaders); - this.wsServer.register(require('hapi-plugin-websocket'), err => err && this.serverlessLog(err)); + this.wsServer.ext('onPreResponse', (request, h) => { + if (request.headers.origin) { + const response = request.response.isBoom ? request.response.output : request.response; + + response.headers['access-control-allow-origin'] = request.headers.origin; + response.headers['access-control-allow-credentials'] = 'true'; + + if (request.method === 'options') { + response.statusCode = 200; + response.headers['access-control-expose-headers'] = 'content-type, content-length, etag'; + response.headers['access-control-max-age'] = 60 * 10; + + if (request.headers['access-control-request-headers']) { + response.headers['access-control-allow-headers'] = request.headers['access-control-request-headers']; + } + + if (request.headers['access-control-request-method']) { + response.headers['access-control-allow-methods'] = request.headers['access-control-request-method']; + } + } + } + + return h.continue; + }); + // end COPY PASTE FROM HTTP SERVER CODE + + this.wsServer.register(require('hapi-plugin-websocket')).catch(err => err && this.serverlessLog(err)); const doAction = (ws, connectionId, name, event, context, doDeafultAction/* , onError */) => { let action = this.wsActions[name]; if (!action && doDeafultAction) action = this.wsActions.$default; if (!action) return; - action.handler(event, context, () => {}).catch(() => { + action.handler(event, context, () => {}).catch(err => { + debugLog(`Error in handler of action ${action}`, err); if (ws.readyState === /* OPEN */1) ws.send(JSON.stringify({ message:'Internal server error', connectionId, requestId:'1234567890' })); }); }; @@ -549,7 +570,7 @@ class Offline { plugins: { websocket: { only: true, - initially: true, + initially: false, connect: ({ ws, req }) => { const parseQuery = queryString => { const query = {}; const parts = queryString.split('?'); @@ -565,6 +586,8 @@ class Offline { const queryStringParameters = parseQuery(req.url); const connection = { connectionId:utils.randomId(), connectionTime:Date.now() }; + debugLog(`connect:${connection.connectionId}`); + this.clients.set(ws, connection); let event = createConnectEvent('$connect', 'CONNECT', connection); if (Object.keys(queryStringParameters).length > 0) event = { queryStringParameters, ...event }; @@ -574,6 +597,7 @@ class Offline { }, disconnect: ({ ws }) => { const connection = this.clients.get(ws); + debugLog(`disconnect:${connection.connectionId}`); this.clients.delete(ws); const event = createDisconnectEvent('$disconnect', 'DISCONNECT', connection); const context = createContext('$disconnect'); @@ -583,30 +607,31 @@ class Offline { }, }, }, - handler: request /* , reply */ => { + handler: (request, h) => { const { initially, ws } = request.websocket(); - if (!request.payload || initially) return; + if (!request.payload || initially) return h.response().code(204); const connection = this.clients.get(ws); const action = request.payload.action || '$default'; + debugLog(`action:${action} on connection=${connection.connectionId}`); const event = createEvent(action, 'MESSAGE', connection, request.payload); const context = createContext(action); + doAction(ws, connection.connectionId, action, event, context, true); + + return h.response().code(204); }, }); this.wsServer.route({ method: 'GET', path: '/{path*}', - handler: (request, reply) => { - const response = reply.response().hold(); - response.statusCode = 426; - response.send(); - }, + handler: (request, h) => h.response().code(426), }); this.wsServer.route({ method: 'POST', path: '/@connections/{connectionId}', config: { payload: { parse: false } }, - handler: (request, reply) => { + handler: (request, h) => { + debugLog(`got POST to ${request.url}`); const getByConnectionId = (map, searchValue) => { for (const [key, connection] of map.entries()) { if (connection.connectionId === searchValue) return key; @@ -614,18 +639,15 @@ class Offline { return undefined; }; - - const response = reply.response().hold(); + const ws = getByConnectionId(this.clients, request.params.connectionId); - if (!ws) { - response.statusCode = 410; - response.send(); - - return; - } - if (!request.payload) return; + if (!ws) return h.response().code(410); + if (!request.payload) return ''; ws.send(request.payload.toString()); - response.send(); + // console.log(`sent "${request.payload.toString().substring}" to ${request.params.connectionId}`); + debugLog(`sent data to connection:${request.params.connectionId}`); + + return ''; }, }); } @@ -1368,31 +1390,27 @@ class Offline { await this.server.start(); } catch (e) { - console.error('Unexpected error while starting serverless-offline server:', e); + console.error(`Unexpected error while starting serverless-offline server on port ${this.options.port}:`, e); process.exit(1); } this.printBlankLine(); this.serverlessLog(`Offline listening on http${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port}`); - resolve(this.server); - }); - }); - await new Promise((resolve, reject) => { - this.wsServer.start(err => { - if (err) return reject(err); + try { + await this.wsServer.start(); + } + catch (e) { + console.error(`Unexpected error while starting serverless-offline server on port ${this.options.port + 1}:`, e); + process.exit(1); + } - this.printBlankLine(); - this.serverlessLog(`Offline listening on ws${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port + 1}`); + this.printBlankLine(); + this.serverlessLog(`Offline listening on ws${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port + 1}`); - this.printBlankLine(); - this.serverlessLog(`Offline listening on http${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port + 1}/@connections/{connectionId}`); + this.printBlankLine(); + this.serverlessLog(`Offline listening on http${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port + 1}/@connections/{connectionId}`); - resolve(this.wsServer); - }); - }); - - return this.server; } end() { From 6638b7317ce7f54bedc6831604f3a32204b9b23e Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Wed, 5 Jun 2019 01:03:12 +0300 Subject: [PATCH 20/71] Merge from master and fixes --- manual_test_websocket/package-lock.json | 6 +++--- manual_test_websocket/package.json | 2 +- src/index.js | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/manual_test_websocket/package-lock.json b/manual_test_websocket/package-lock.json index 6373297df..718b8b461 100644 --- a/manual_test_websocket/package-lock.json +++ b/manual_test_websocket/package-lock.json @@ -81,9 +81,9 @@ "dev": true }, "aws-sdk": { - "version": "2.466.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.466.0.tgz", - "integrity": "sha512-dWFpz774ONjP1Cb19VkLOfQSVTu5p5/uncZGovAe71NOfPGDSvrQKXOsKcuI1/k4oJyKW9z/GATF8ht8DkDWGg==", + "version": "2.468.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.468.0.tgz", + "integrity": "sha512-Bo4j1DLDBWSLgNsfpLNU2RKU2+24JzdFqgyyOKOyJ1p6RgrnDxcwoR2CPWK5olPU2cgXmIicetfOGDsC3LjLtg==", "requires": { "buffer": "4.9.1", "events": "1.1.1", diff --git a/manual_test_websocket/package.json b/manual_test_websocket/package.json index 13aa78a7a..191802f30 100644 --- a/manual_test_websocket/package.json +++ b/manual_test_websocket/package.json @@ -13,7 +13,7 @@ "author": "", "license": "MIT", "dependencies": { - "aws-sdk": "^2.466.0" + "aws-sdk": "^2.468.0" }, "devDependencies": { "aws4": "^1.8.0", diff --git a/src/index.js b/src/index.js index a873e186f..89e0c223a 100755 --- a/src/index.js +++ b/src/index.js @@ -462,9 +462,9 @@ class Offline { const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; const requestContext = { routeKey: action, - messageId: `${utils.randomId()}`, + messageId: `${randomId()}`, eventType, - extendedRequestId: `${utils.randomId()}`, + extendedRequestId: `${randomId()}`, requestTime: `${now.getUTCDate()}/${months[now.getUTCMonth()]}/${now.getUTCFullYear()}:${now.getUTCHours()}:${now.getUTCMinutes()}:${now.getSeconds()} +0000`, messageDirection: 'IN', stage: 'local', @@ -483,7 +483,7 @@ class Offline { accessKey: null, cognitoAuthenticationProvider: null, user: null }, - requestId: `${utils.randomId()}`, + requestId: `${randomId()}`, domainName: 'localhost', connectionId:connection.connectionId, apiId: 'private', @@ -507,9 +507,9 @@ class Offline { const headers = { Host: 'localhost', 'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits', - 'Sec-WebSocket-Key': `${utils.randomId()}`, + 'Sec-WebSocket-Key': `${randomId()}`, 'Sec-WebSocket-Version': '13', - 'X-Amzn-Trace-Id': `Root=${utils.randomId()}`, + 'X-Amzn-Trace-Id': `Root=${randomId()}`, 'X-Forwarded-For': '127.0.0.1', 'X-Forwarded-Port': `${this.options.port + 1}`, 'X-Forwarded-Proto': `http${this.options.httpsProtocol ? 's' : ''}`, @@ -585,7 +585,7 @@ class Offline { }; const queryStringParameters = parseQuery(req.url); - const connection = { connectionId:utils.randomId(), connectionTime:Date.now() }; + const connection = { connectionId:randomId(), connectionTime:Date.now() }; debugLog(`connect:${connection.connectionId}`); this.clients.set(ws, connection); From b51ddb532f7fd36d67ff6b8d9076476c86464cd1 Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Wed, 5 Jun 2019 10:44:23 +0300 Subject: [PATCH 21/71] added support for callback() in handler --- manual_test_websocket/handler.js | 9 +++++++ manual_test_websocket/scripts/serverless..yml | 10 ++++++++ manual_test_websocket/test/e2e/ws.e2e.js | 18 ++++++++++++-- src/index.js | 24 +++++++++++++++---- 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/manual_test_websocket/handler.js b/manual_test_websocket/handler.js index 8b75497e4..58dbeb16c 100644 --- a/manual_test_websocket/handler.js +++ b/manual_test_websocket/handler.js @@ -64,6 +64,15 @@ module.exports.makeError = async (event, context) => { return successfullResponse; }; +module.exports.replyViaCallback = (event, context, callback) => { + sendToClient({action:'update', event:'reply-via-callback'}, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err=>console.log(err)); + callback(); +}; + +module.exports.replyErrorViaCallback = (event, context, callback) => { + return callback("error error error"); +}; + module.exports.multiCall1 = async (event, context) => { await sendToClient({action:'update', event:'made-call-1'}, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err=>console.log(err)); return successfullResponse; diff --git a/manual_test_websocket/scripts/serverless..yml b/manual_test_websocket/scripts/serverless..yml index 62809f21d..02ecbadcc 100644 --- a/manual_test_websocket/scripts/serverless..yml +++ b/manual_test_websocket/scripts/serverless..yml @@ -71,6 +71,16 @@ functions: events: - websocket: route: makeError + replyViaCallback: + handler: handler.replyViaCallback + events: + - websocket: + route: replyViaCallback + replyErrorViaCallback: + handler: handler.replyErrorViaCallback + events: + - websocket: + route: replyErrorViaCallback multiCall1: handler: handler.multiCall1 events: diff --git a/manual_test_websocket/test/e2e/ws.e2e.js b/manual_test_websocket/test/e2e/ws.e2e.js index 1ef3aa91c..d8db0b706 100644 --- a/manual_test_websocket/test/e2e/ws.e2e.js +++ b/manual_test_websocket/test/e2e/ws.e2e.js @@ -94,14 +94,28 @@ describe('serverless', ()=>{ expect(await c3.ws.receive1()).to.equal('Hello World!'); }).timeout(timeout); - it('should response when having an internal server error', async ()=>{ + it('should respond when having an internal server error', async ()=>{ const conn=await createClient(); conn.ws.send(JSON.stringify({action:'makeError'})); const res=JSON.parse(await conn.ws.receive1()); expect(res).to.deep.equal({message:'Internal server error', connectionId:conn.id, requestId:res.requestId}); }).timeout(timeout); - it('should response with only the last action when there are more than one in the serverless.yml file', async ()=>{ + it('should respond via callback', async ()=>{ + const ws=await createWebSocket(); + ws.send(JSON.stringify({action:'replyViaCallback'})); + const res=JSON.parse(await ws.receive1()); + expect(res).to.deep.equal({action:'update', event:'reply-via-callback'}); + }).timeout(timeout); + + it('should respond with error when calling callback(error)', async ()=>{ + const conn=await createClient(); + conn.ws.send(JSON.stringify({action:'replyErrorViaCallback'})); + const res=JSON.parse(await conn.ws.receive1()); + expect(res).to.deep.equal({message:'Internal server error', connectionId:conn.id, requestId:res.requestId}); + }).timeout(timeout); + + it('should respond with only the last action when there are more than one in the serverless.yml file', async ()=>{ const ws=await createWebSocket(); ws.send(JSON.stringify({action:'makeMultiCalls'})); const res=JSON.parse(await ws.receive1()); diff --git a/src/index.js b/src/index.js index 89e0c223a..c15768e53 100755 --- a/src/index.js +++ b/src/index.js @@ -448,13 +448,29 @@ class Offline { this.wsServer.register(require('hapi-plugin-websocket')).catch(err => err && this.serverlessLog(err)); const doAction = (ws, connectionId, name, event, context, doDeafultAction/* , onError */) => { + const sendError = err => { + if (ws.readyState === /* OPEN */1) ws.send(JSON.stringify({ message:'Internal server error', connectionId, requestId:'1234567890' })); + debugLog(`Error in handler of action ${action}`, err); + }; let action = this.wsActions[name]; if (!action && doDeafultAction) action = this.wsActions.$default; if (!action) return; - action.handler(event, context, () => {}).catch(err => { - debugLog(`Error in handler of action ${action}`, err); - if (ws.readyState === /* OPEN */1) ws.send(JSON.stringify({ message:'Internal server error', connectionId, requestId:'1234567890' })); - }); + let p = null; + try { + p = action.handler(event, context, err => { + if (!err) return; + sendError(err); + }); + } + catch (err) { + sendError(err); + } + + if (p) { + p.catch(err => { + sendError(err); + }); + } }; const createRequestContext = (action, eventType, connection) => { From 381355baed0cf80e2a1d8182c7eaf6cef635494a Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Thu, 6 Jun 2019 08:32:34 +0300 Subject: [PATCH 22/71] Moved websocket CreateXXX to websocketHelpers.js --- manual_test_websocket/handler.js | 25 ++++ manual_test_websocket/package-lock.json | 6 +- manual_test_websocket/package.json | 2 +- manual_test_websocket/scripts/serverless..yml | 3 + src/index.js | 118 ++---------------- src/websocketHelpers.js | 106 ++++++++++++++++ 6 files changed, 145 insertions(+), 115 deletions(-) create mode 100644 src/websocketHelpers.js diff --git a/manual_test_websocket/handler.js b/manual_test_websocket/handler.js index 58dbeb16c..7ada0f080 100644 --- a/manual_test_websocket/handler.js +++ b/manual_test_websocket/handler.js @@ -17,6 +17,23 @@ const errorResponse = { body: 'Request is not OK.' }; +// const generatePolicy = function(principalId, effect, resource) { +// const authResponse = {}; +// authResponse.principalId = principalId; +// if (effect && resource) { +// const policyDocument = {}; +// policyDocument.Version = '2012-10-17'; +// policyDocument.Statement = []; +// const statementOne = {}; +// statementOne.Action = 'execute-api:Invoke'; +// statementOne.Effect = effect; +// statementOne.Resource = resource; +// policyDocument.Statement[0] = statementOne; +// authResponse.policyDocument = policyDocument; +// } +// return authResponse; +// }; + // module.exports.http = async (event, context) => { // return successfullResponse; // }; @@ -36,6 +53,14 @@ module.exports.connect = async (event, context) => { return successfullResponse; }; +// module.export.auth = (event, context, callback) => { +// //console.log('auth:'); +// const token = event.headers["Authorization"]; + +// if ('deny'===token) callback(null, generatePolicy('user', 'Deny', event.methodArn)); +// else callback(null, generatePolicy('user', 'Allow', event.methodArn));; +// }; + module.exports.disconnect = async (event, context) => { const listener=await ddb.get({TableName:'listeners', Key:{name:'default'}}).promise(); if (listener.Item) await sendToClient(JSON.stringify({action:'update', event:'disconnect', info:{id:event.requestContext.connectionId, event:{...event, apiGatewayUrl:`${event.apiGatewayUrl}`}, context}}), listener.Item.id, newAWSApiGatewayManagementApi(event, context)).catch(()=>{}); diff --git a/manual_test_websocket/package-lock.json b/manual_test_websocket/package-lock.json index 718b8b461..4c445744f 100644 --- a/manual_test_websocket/package-lock.json +++ b/manual_test_websocket/package-lock.json @@ -81,9 +81,9 @@ "dev": true }, "aws-sdk": { - "version": "2.468.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.468.0.tgz", - "integrity": "sha512-Bo4j1DLDBWSLgNsfpLNU2RKU2+24JzdFqgyyOKOyJ1p6RgrnDxcwoR2CPWK5olPU2cgXmIicetfOGDsC3LjLtg==", + "version": "2.469.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.469.0.tgz", + "integrity": "sha512-VaIrO3aBX83gKkBPk9xM0RHmu7fmq76kaF0SqbsWlPImgxc5foJ4rBlRMMlmeNogFZZ/XTQdI+gkFDVosV14Ig==", "requires": { "buffer": "4.9.1", "events": "1.1.1", diff --git a/manual_test_websocket/package.json b/manual_test_websocket/package.json index 191802f30..2d492794a 100644 --- a/manual_test_websocket/package.json +++ b/manual_test_websocket/package.json @@ -13,7 +13,7 @@ "author": "", "license": "MIT", "dependencies": { - "aws-sdk": "^2.468.0" + "aws-sdk": "^2.469.0" }, "devDependencies": { "aws4": "^1.8.0", diff --git a/manual_test_websocket/scripts/serverless..yml b/manual_test_websocket/scripts/serverless..yml index 02ecbadcc..3c448fb3d 100644 --- a/manual_test_websocket/scripts/serverless..yml +++ b/manual_test_websocket/scripts/serverless..yml @@ -46,6 +46,9 @@ functions: events: - websocket: route: $connect + # authorizer: auth + # auth: + # handler: handler.auth disconnect: handler: handler.disconnect events: diff --git a/src/index.js b/src/index.js index c15768e53..d778345a3 100755 --- a/src/index.js +++ b/src/index.js @@ -22,6 +22,7 @@ const parseResources = require('./parseResources'); const { createDefaultApiKey, detectEncoding, randomId } = require('./utils'); const authFunctionNameExtractor = require('./authFunctionNameExtractor'); const requestBodyValidator = require('./requestBodyValidator'); +const wsHelpers = require('./websocketHelpers'); /* I'm against monolithic code like this file @@ -472,111 +473,6 @@ class Offline { }); } }; - - const createRequestContext = (action, eventType, connection) => { - const now = new Date(); - const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; - const requestContext = { - routeKey: action, - messageId: `${randomId()}`, - eventType, - extendedRequestId: `${randomId()}`, - requestTime: `${now.getUTCDate()}/${months[now.getUTCMonth()]}/${now.getUTCFullYear()}:${now.getUTCHours()}:${now.getUTCMinutes()}:${now.getSeconds()} +0000`, - messageDirection: 'IN', - stage: 'local', - connectedAt: connection.connectionTime, - requestTimeEpoch: now.getTime(), - identity: - { cognitoIdentityPoolId: null, - cognitoIdentityId: null, - principalOrgId: null, - cognitoAuthenticationType: null, - userArn: null, - userAgent: null, - accountId: null, - caller: null, - sourceIp: '127.0.0.1', - accessKey: null, - cognitoAuthenticationProvider: null, - user: null }, - requestId: `${randomId()}`, - domainName: 'localhost', - connectionId:connection.connectionId, - apiId: 'private', - }; - - return requestContext; - }; - - const createEvent = (action, eventType, connection, payload) => { - const event = { - requestContext: createRequestContext(action, eventType, connection), - body: JSON.stringify(payload), - isBase64Encoded: false, - apiGatewayUrl: `http${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port + 1}`, - }; - - return event; - }; - - const createConnectEvent = (action, eventType, connection) => { - const headers = { - Host: 'localhost', - 'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits', - 'Sec-WebSocket-Key': `${randomId()}`, - 'Sec-WebSocket-Version': '13', - 'X-Amzn-Trace-Id': `Root=${randomId()}`, - 'X-Forwarded-For': '127.0.0.1', - 'X-Forwarded-Port': `${this.options.port + 1}`, - 'X-Forwarded-Proto': `http${this.options.httpsProtocol ? 's' : ''}`, - }; - const multiValueHeaders = { ...headers }; - Object.keys(multiValueHeaders).map(key => multiValueHeaders[key] = [multiValueHeaders[key]]); - const event = { - headers, - multiValueHeaders, - requestContext: createRequestContext(action, eventType, connection), - apiGatewayUrl: `http${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port + 1}`, - isBase64Encoded: false, - }; - - return event; - }; - - const createDisconnectEvent = (action, eventType, connection) => { - const headers = { - Host: 'localhost', - 'x-api-key': '', - 'x-restapi': '', - }; - const multiValueHeaders = { ...headers }; - Object.keys(multiValueHeaders).map(key => multiValueHeaders[key] = [multiValueHeaders[key]]); - const event = { - headers, - multiValueHeaders, - requestContext: createRequestContext(action, eventType, connection), - apiGatewayUrl: `http${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port + 1}`, - isBase64Encoded: false, - }; - - return event; - }; - - const createContext = action => { - const context = { - awsRequestId: `offline_awsRequestId_for_${action}`, - callbackWaitsForEmptyEventLoop: true, - functionName: action, - functionVersion: '$LATEST', - invokedFunctionArn: `offline_invokedFunctionArn_for_${action}`, - invokeid: `offline_invokeid_for_${action}`, - logGroupName: `offline_logGroupName_for_${action}`, - logStreamName: `offline_logStreamName_for_${action}`, - memoryLimitInMB: '1024', - }; - - return context; - }; this.wsServer.route({ method: 'POST', @@ -605,9 +501,9 @@ class Offline { debugLog(`connect:${connection.connectionId}`); this.clients.set(ws, connection); - let event = createConnectEvent('$connect', 'CONNECT', connection); + let event = wsHelpers.createConnectEvent('$connect', 'CONNECT', connection, this.options); if (Object.keys(queryStringParameters).length > 0) event = { queryStringParameters, ...event }; - const context = createContext('$connect'); + const context = wsHelpers.createContext('$connect'); doAction(ws, connection.connectionId, '$connect', event, context); }, @@ -615,8 +511,8 @@ class Offline { const connection = this.clients.get(ws); debugLog(`disconnect:${connection.connectionId}`); this.clients.delete(ws); - const event = createDisconnectEvent('$disconnect', 'DISCONNECT', connection); - const context = createContext('$disconnect'); + const event = wsHelpers.createDisconnectEvent('$disconnect', 'DISCONNECT', connection, this.options); + const context = wsHelpers.createContext('$disconnect', this.options); doAction(ws, connection.connectionId, '$disconnect', event, context); }, @@ -629,8 +525,8 @@ class Offline { const connection = this.clients.get(ws); const action = request.payload.action || '$default'; debugLog(`action:${action} on connection=${connection.connectionId}`); - const event = createEvent(action, 'MESSAGE', connection, request.payload); - const context = createContext(action); + const event = wsHelpers.createEvent(action, 'MESSAGE', connection, request.payload, this.options); + const context = wsHelpers.createContext(action, this.options); doAction(ws, connection.connectionId, action, event, context, true); diff --git a/src/websocketHelpers.js b/src/websocketHelpers.js new file mode 100644 index 000000000..bb4303e5b --- /dev/null +++ b/src/websocketHelpers.js @@ -0,0 +1,106 @@ +const { randomId } = require('./utils'); + +const createRequestContext = (action, eventType, connection) => { + const now = new Date(); + const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + const requestContext = { + routeKey: action, + messageId: `${randomId()}`, + eventType, + extendedRequestId: `${randomId()}`, + requestTime: `${now.getUTCDate()}/${months[now.getUTCMonth()]}/${now.getUTCFullYear()}:${now.getUTCHours()}:${now.getUTCMinutes()}:${now.getSeconds()} +0000`, + messageDirection: 'IN', + stage: 'local', + connectedAt: connection.connectionTime, + requestTimeEpoch: now.getTime(), + identity: + { cognitoIdentityPoolId: null, + cognitoIdentityId: null, + principalOrgId: null, + cognitoAuthenticationType: null, + userArn: null, + userAgent: null, + accountId: null, + caller: null, + sourceIp: '127.0.0.1', + accessKey: null, + cognitoAuthenticationProvider: null, + user: null }, + requestId: `${randomId()}`, + domainName: 'localhost', + connectionId:connection.connectionId, + apiId: 'private', + }; + + return requestContext; +}; + +module.exports.createEvent = (action, eventType, connection, payload, options) => { + const event = { + requestContext: createRequestContext(action, eventType, connection), + body: JSON.stringify(payload), + isBase64Encoded: false, + apiGatewayUrl: `http${options.httpsProtocol ? 's' : ''}://${options.host}:${options.port + 1}`, + }; + + return event; +}; + +module.exports.createConnectEvent = (action, eventType, connection, options) => { + const headers = { + Host: 'localhost', + 'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits', + 'Sec-WebSocket-Key': `${randomId()}`, + 'Sec-WebSocket-Version': '13', + 'X-Amzn-Trace-Id': `Root=${randomId()}`, + 'X-Forwarded-For': '127.0.0.1', + 'X-Forwarded-Port': `${options.port + 1}`, + 'X-Forwarded-Proto': `http${options.httpsProtocol ? 's' : ''}`, + }; + const multiValueHeaders = { ...headers }; + Object.keys(multiValueHeaders).map(key => multiValueHeaders[key] = [multiValueHeaders[key]]); + const event = { + headers, + multiValueHeaders, + requestContext: createRequestContext(action, eventType, connection), + apiGatewayUrl: `http${options.httpsProtocol ? 's' : ''}://${options.host}:${options.port + 1}`, + isBase64Encoded: false, + }; + + return event; +}; + +module.exports.createDisconnectEvent = (action, eventType, connection, options) => { + const headers = { + Host: 'localhost', + 'x-api-key': '', + 'x-restapi': '', + }; + const multiValueHeaders = { ...headers }; + Object.keys(multiValueHeaders).map(key => multiValueHeaders[key] = [multiValueHeaders[key]]); + const event = { + headers, + multiValueHeaders, + requestContext: createRequestContext(action, eventType, connection), + apiGatewayUrl: `http${options.httpsProtocol ? 's' : ''}://${options.host}:${options.port + 1}`, + isBase64Encoded: false, + }; + + return event; +}; + +module.exports.createContext = action => { + const context = { + awsRequestId: `offline_awsRequestId_for_${action}`, + callbackWaitsForEmptyEventLoop: true, + functionName: action, + functionVersion: '$LATEST', + invokedFunctionArn: `offline_invokedFunctionArn_for_${action}`, + invokeid: `offline_invokeid_for_${action}`, + logGroupName: `offline_logGroupName_for_${action}`, + logStreamName: `offline_logStreamName_for_${action}`, + memoryLimitInMB: '1024', + }; + + return context; +}; From d45ea7ae305aafbb6f617a207b2a6f9aa62adae3 Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Thu, 6 Jun 2019 22:12:24 +0300 Subject: [PATCH 23/71] Restructure manual_test_websocket to include more projects --- .../{ => manual_test_websocket_main}/handler.js | 0 .../{ => manual_test_websocket_main}/package-lock.json | 0 .../{ => manual_test_websocket_main}/package.json | 0 .../{ => manual_test_websocket_main}/scripts/deploy_to_aws.sh | 0 .../scripts/deploy_to_offline.sh | 0 .../{ => manual_test_websocket_main}/scripts/serverless..yml | 2 +- .../{ => manual_test_websocket_main}/scripts/serverless.aws.yml | 0 .../scripts/serverless.offline.yml | 0 .../{ => manual_test_websocket_main}/serverless.yml | 0 .../{ => manual_test_websocket_main}/test/e2e/ws.e2e.js | 0 .../test/support/WebSocketTester.js | 0 11 files changed, 1 insertion(+), 1 deletion(-) rename manual_test_websocket/{ => manual_test_websocket_main}/handler.js (100%) rename manual_test_websocket/{ => manual_test_websocket_main}/package-lock.json (100%) rename manual_test_websocket/{ => manual_test_websocket_main}/package.json (100%) rename manual_test_websocket/{ => manual_test_websocket_main}/scripts/deploy_to_aws.sh (100%) rename manual_test_websocket/{ => manual_test_websocket_main}/scripts/deploy_to_offline.sh (100%) rename manual_test_websocket/{ => manual_test_websocket_main}/scripts/serverless..yml (98%) rename manual_test_websocket/{ => manual_test_websocket_main}/scripts/serverless.aws.yml (100%) rename manual_test_websocket/{ => manual_test_websocket_main}/scripts/serverless.offline.yml (100%) rename manual_test_websocket/{ => manual_test_websocket_main}/serverless.yml (100%) rename manual_test_websocket/{ => manual_test_websocket_main}/test/e2e/ws.e2e.js (100%) rename manual_test_websocket/{ => manual_test_websocket_main}/test/support/WebSocketTester.js (100%) diff --git a/manual_test_websocket/handler.js b/manual_test_websocket/manual_test_websocket_main/handler.js similarity index 100% rename from manual_test_websocket/handler.js rename to manual_test_websocket/manual_test_websocket_main/handler.js diff --git a/manual_test_websocket/package-lock.json b/manual_test_websocket/manual_test_websocket_main/package-lock.json similarity index 100% rename from manual_test_websocket/package-lock.json rename to manual_test_websocket/manual_test_websocket_main/package-lock.json diff --git a/manual_test_websocket/package.json b/manual_test_websocket/manual_test_websocket_main/package.json similarity index 100% rename from manual_test_websocket/package.json rename to manual_test_websocket/manual_test_websocket_main/package.json diff --git a/manual_test_websocket/scripts/deploy_to_aws.sh b/manual_test_websocket/manual_test_websocket_main/scripts/deploy_to_aws.sh similarity index 100% rename from manual_test_websocket/scripts/deploy_to_aws.sh rename to manual_test_websocket/manual_test_websocket_main/scripts/deploy_to_aws.sh diff --git a/manual_test_websocket/scripts/deploy_to_offline.sh b/manual_test_websocket/manual_test_websocket_main/scripts/deploy_to_offline.sh similarity index 100% rename from manual_test_websocket/scripts/deploy_to_offline.sh rename to manual_test_websocket/manual_test_websocket_main/scripts/deploy_to_offline.sh diff --git a/manual_test_websocket/scripts/serverless..yml b/manual_test_websocket/manual_test_websocket_main/scripts/serverless..yml similarity index 98% rename from manual_test_websocket/scripts/serverless..yml rename to manual_test_websocket/manual_test_websocket_main/scripts/serverless..yml index 3c448fb3d..729d3ad14 100644 --- a/manual_test_websocket/scripts/serverless..yml +++ b/manual_test_websocket/manual_test_websocket_main/scripts/serverless..yml @@ -11,7 +11,7 @@ # # Happy Coding! -service: manual-test-websocket +service: manual-test-websocket-main provider: name: aws diff --git a/manual_test_websocket/scripts/serverless.aws.yml b/manual_test_websocket/manual_test_websocket_main/scripts/serverless.aws.yml similarity index 100% rename from manual_test_websocket/scripts/serverless.aws.yml rename to manual_test_websocket/manual_test_websocket_main/scripts/serverless.aws.yml diff --git a/manual_test_websocket/scripts/serverless.offline.yml b/manual_test_websocket/manual_test_websocket_main/scripts/serverless.offline.yml similarity index 100% rename from manual_test_websocket/scripts/serverless.offline.yml rename to manual_test_websocket/manual_test_websocket_main/scripts/serverless.offline.yml diff --git a/manual_test_websocket/serverless.yml b/manual_test_websocket/manual_test_websocket_main/serverless.yml similarity index 100% rename from manual_test_websocket/serverless.yml rename to manual_test_websocket/manual_test_websocket_main/serverless.yml diff --git a/manual_test_websocket/test/e2e/ws.e2e.js b/manual_test_websocket/manual_test_websocket_main/test/e2e/ws.e2e.js similarity index 100% rename from manual_test_websocket/test/e2e/ws.e2e.js rename to manual_test_websocket/manual_test_websocket_main/test/e2e/ws.e2e.js diff --git a/manual_test_websocket/test/support/WebSocketTester.js b/manual_test_websocket/manual_test_websocket_main/test/support/WebSocketTester.js similarity index 100% rename from manual_test_websocket/test/support/WebSocketTester.js rename to manual_test_websocket/manual_test_websocket_main/test/support/WebSocketTester.js From 66ffe6ab000823870a2d3c1aadf929a30b25f8b9 Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Thu, 6 Jun 2019 22:58:28 +0300 Subject: [PATCH 24/71] Added serverless.yml with warning --- .../handler.js | 60 + .../package-lock.json | 1488 +++++++++++++++++ .../package.json | 28 + .../scripts/deploy_to_aws.sh | 12 + .../scripts/deploy_to_offline.sh | 14 + .../scripts/serverless..yml | 35 + .../scripts/serverless.aws.yml | 9 + .../scripts/serverless.offline.yml | 4 + .../serverless.yml | 7 + .../test/e2e/ws.e2e.js | 342 ++++ .../test/support/WebSocketTester.js | 60 + 11 files changed, 2059 insertions(+) create mode 100644 manual_test_websocket/manual_test_websocket_authorizer/handler.js create mode 100644 manual_test_websocket/manual_test_websocket_authorizer/package-lock.json create mode 100644 manual_test_websocket/manual_test_websocket_authorizer/package.json create mode 100755 manual_test_websocket/manual_test_websocket_authorizer/scripts/deploy_to_aws.sh create mode 100755 manual_test_websocket/manual_test_websocket_authorizer/scripts/deploy_to_offline.sh create mode 100644 manual_test_websocket/manual_test_websocket_authorizer/scripts/serverless..yml create mode 100644 manual_test_websocket/manual_test_websocket_authorizer/scripts/serverless.aws.yml create mode 100644 manual_test_websocket/manual_test_websocket_authorizer/scripts/serverless.offline.yml create mode 100644 manual_test_websocket/manual_test_websocket_authorizer/serverless.yml create mode 100644 manual_test_websocket/manual_test_websocket_authorizer/test/e2e/ws.e2e.js create mode 100644 manual_test_websocket/manual_test_websocket_authorizer/test/support/WebSocketTester.js diff --git a/manual_test_websocket/manual_test_websocket_authorizer/handler.js b/manual_test_websocket/manual_test_websocket_authorizer/handler.js new file mode 100644 index 000000000..b433807be --- /dev/null +++ b/manual_test_websocket/manual_test_websocket_authorizer/handler.js @@ -0,0 +1,60 @@ +'use strict'; + +const AWS = require('aws-sdk'); + +const successfullResponse = { + statusCode: 200, + body: 'Request is OK.' +}; + +const errorResponse = { + statusCode: 400, + body: 'Request is not OK.' +}; + +const generatePolicy = function(principalId, effect, resource) { + const authResponse = {}; + authResponse.principalId = principalId; + if (effect && resource) { + const policyDocument = {}; + policyDocument.Version = '2012-10-17'; + policyDocument.Statement = []; + const statementOne = {}; + statementOne.Action = 'execute-api:Invoke'; + statementOne.Effect = effect; + statementOne.Resource = resource; + policyDocument.Statement[0] = statementOne; + authResponse.policyDocument = policyDocument; + } + return authResponse; +}; + + +module.exports.connect = async (event, context) => { + // console.log('connect:'); + return successfullResponse; +}; + +module.export.auth = (event, context, callback) => { + //console.log('auth:'); + const token = event.headers["Authorization"]; + + if ('deny'===token) callback(null, generatePolicy('user', 'Deny', event.methodArn)); + else callback(null, generatePolicy('user', 'Allow', event.methodArn));; +}; + +const newAWSApiGatewayManagementApi=(event, context)=>{ + let endpoint=event.apiGatewayUrl; + + if (!endpoint) endpoint = event.requestContext.domainName+'/'+event.requestContext.stage; + const apiVersion='2018-11-29'; + return new AWS.ApiGatewayManagementApi({ apiVersion, endpoint }); +}; + +const sendToClient = (data, connectionId, apigwManagementApi) => { + // console.log(`sendToClient:${connectionId}`); + let sendee=data; + if ('object'==typeof data) sendee=JSON.stringify(data); + + return apigwManagementApi.postToConnection({ConnectionId: connectionId, Data: sendee}).promise(); +}; diff --git a/manual_test_websocket/manual_test_websocket_authorizer/package-lock.json b/manual_test_websocket/manual_test_websocket_authorizer/package-lock.json new file mode 100644 index 000000000..4c445744f --- /dev/null +++ b/manual_test_websocket/manual_test_websocket_authorizer/package-lock.json @@ -0,0 +1,1488 @@ +{ + "name": "manual_test", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/chai": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz", + "integrity": "sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==", + "dev": true + }, + "@types/cookiejar": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.1.tgz", + "integrity": "sha512-aRnpPa7ysx3aNW60hTiCtLHlQaIFsXFCgQlpakNgDNVFzbtusSY8PwjAQgRWfSk0ekNoBjO51eQRB6upA9uuyw==", + "dev": true + }, + "@types/node": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz", + "integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==", + "dev": true + }, + "@types/superagent": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-3.8.7.tgz", + "integrity": "sha512-9KhCkyXv268A2nZ1Wvu7rQWM+BmdYUVkycFeNnYrUL5Zwu7o8wPQ3wBfW59dDP+wuoxw0ww8YKgTNv8j/cgscA==", + "dev": true, + "requires": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, + "ansi-colors": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", + "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", + "dev": true + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "aws-sdk": { + "version": "2.469.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.469.0.tgz", + "integrity": "sha512-VaIrO3aBX83gKkBPk9xM0RHmu7fmq76kaF0SqbsWlPImgxc5foJ4rBlRMMlmeNogFZZ/XTQdI+gkFDVosV14Ig==", + "requires": { + "buffer": "4.9.1", + "events": "1.1.1", + "ieee754": "1.1.8", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.3.2", + "xml2js": "0.4.19" + } + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "dev": true + }, + "awscred": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/awscred/-/awscred-1.4.2.tgz", + "integrity": "sha512-j3Vehf6PCFzzPZKkzEcj0Y2QO8w8UBbgobnl3DwHMiAE9A2mfJxTkq3cX4UNWHmrTAR0rj5BC/ts90Ok4Pg6rw==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "dev": true, + "requires": { + "inherits": "~2.0.0" + } + }, + "bluebird": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.4.tgz", + "integrity": "sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, + "chai-http": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/chai-http/-/chai-http-4.3.0.tgz", + "integrity": "sha512-zFTxlN7HLMv+7+SPXZdkd5wUlK+KxH6Q7bIEMiEx0FK3zuuMqL7cwICAQ0V1+yYRozBburYuxN1qZstgHpFZQg==", + "dev": true, + "requires": { + "@types/chai": "4", + "@types/superagent": "^3.8.3", + "cookiejar": "^2.1.1", + "is-ip": "^2.0.0", + "methods": "^1.1.2", + "qs": "^6.5.1", + "superagent": "^3.7.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "dev": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "dynamodb-localhost": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/dynamodb-localhost/-/dynamodb-localhost-0.0.7.tgz", + "integrity": "sha512-Xyv0EqQDuOVjA8XGVOo3SuzQ5jKA8/gBKUeKRP3V586Fh9abWXLXOGjf7mPO8sWzddzGqyQx2mALj9IWSotg7A==", + "dev": true, + "requires": { + "mkdirp": "^0.5.0", + "progress": "^1.1.8", + "rmdir": "^1.2.0", + "tar": "^2.0.0" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "es-abstract": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", + "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-keys": "^1.0.12" + } + }, + "es-to-primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "flat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", + "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", + "dev": true, + "requires": { + "is-buffer": "~2.0.3" + } + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", + "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fstream": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "dev": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "ieee754": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true + }, + "is": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/is/-/is-0.2.7.tgz", + "integrity": "sha1-OzSixI81mXLzUEKEkZOucmS2NWI=", + "dev": true + }, + "is-buffer": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", + "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==", + "dev": true + }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "dev": true + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-2.0.0.tgz", + "integrity": "sha1-aO6gfooKCpTC0IDdZ0xzGrKkYas=", + "dev": true, + "requires": { + "ip-regex": "^2.0.0" + } + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "^1.0.1" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-symbol": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "requires": { + "chalk": "^2.0.1" + } + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + } + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", + "dev": true + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "dev": true, + "requires": { + "mime-db": "1.40.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.4.tgz", + "integrity": "sha512-PN8CIy4RXsIoxoFJzS4QNnCH4psUCPWc4/rPrst/ecSJJbLBkubMiyGCP2Kj/9YnWbotFqAoeXyXMucj7gwCFg==", + "dev": true, + "requires": { + "ansi-colors": "3.2.3", + "browser-stdout": "1.3.1", + "debug": "3.2.6", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "find-up": "3.0.0", + "glob": "7.1.3", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "2.2.0", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "ms": "2.1.1", + "node-environment-flags": "1.0.5", + "object.assign": "4.1.0", + "strip-json-comments": "2.0.1", + "supports-color": "6.0.0", + "which": "1.3.1", + "wide-align": "1.1.3", + "yargs": "13.2.2", + "yargs-parser": "13.0.0", + "yargs-unparser": "1.5.0" + } + }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==", + "dev": true + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-environment-flags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", + "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", + "dev": true, + "requires": { + "object.getownpropertydescriptors": "^2.0.3", + "semver": "^5.7.0" + } + }, + "node.extend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-1.0.8.tgz", + "integrity": "sha1-urBDefc4P0WHmQyd8Htqf2Xbdys=", + "dev": true, + "requires": { + "is": "~0.2.6", + "object-keys": "~0.4.0" + }, + "dependencies": { + "object-keys": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=", + "dev": true + } + } + }, + "node.flow": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/node.flow/-/node.flow-1.2.3.tgz", + "integrity": "sha1-4cRKgq7KjXi0WKd/s9xkLy66Jkk=", + "dev": true, + "requires": { + "node.extend": "1.0.8" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.5.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "dev": true + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "rmdir": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rmdir/-/rmdir-1.2.0.tgz", + "integrity": "sha1-T+A1fLBhaMJY5z6WgJPcTooPMlM=", + "dev": true, + "requires": { + "node.flow": "1.2.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + }, + "serverless-dynamodb-local": { + "version": "0.2.37", + "resolved": "https://registry.npmjs.org/serverless-dynamodb-local/-/serverless-dynamodb-local-0.2.37.tgz", + "integrity": "sha512-1q3rfmn+Y1nZQhmuVdFLJcMTc6la1zY6sT98TS6Af0vRG//oa7CuiDEox0XEzuj7KZ0TodwmXmnRdzt9VRm3fA==", + "dev": true, + "requires": { + "aws-sdk": "^2.7.0", + "bluebird": "^3.4.6", + "dynamodb-localhost": "^0.0.7", + "lodash": "^4.17.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "dev": true, + "requires": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.2.0", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.3.5" + } + }, + "supports-color": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", + "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "tar": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "dev": true, + "requires": { + "block-stream": "*", + "fstream": "^1.0.2", + "inherits": "2" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "ws": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.0.0.tgz", + "integrity": "sha512-cknCal4k0EAOrh1SHHPPWWh4qm93g1IuGGGwBjWkXmCG7LsDtL8w9w+YVfaF+KSVwiHQKDIMsSLBVftKf9d1pg==", + "dev": true, + "requires": { + "async-limiter": "^1.0.0" + } + }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yargs": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", + "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "os-locale": "^3.1.0", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "yargs-parser": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", + "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "yargs-unparser": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz", + "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==", + "dev": true, + "requires": { + "flat": "^4.1.0", + "lodash": "^4.17.11", + "yargs": "^12.0.5" + }, + "dependencies": { + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "yargs": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", + "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" + } + }, + "yargs-parser": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", + "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + } + } +} diff --git a/manual_test_websocket/manual_test_websocket_authorizer/package.json b/manual_test_websocket/manual_test_websocket_authorizer/package.json new file mode 100644 index 000000000..2d492794a --- /dev/null +++ b/manual_test_websocket/manual_test_websocket_authorizer/package.json @@ -0,0 +1,28 @@ +{ + "name": "manual_test", + "version": "0.0.0", + "description": "", + "main": "handler.js", + "scripts": { + "test": "mocha ./test/*", + "test-only": "mocha ", + "start": "sls offline", + "deploy-aws": "./scripts/deploy_to_aws.sh", + "deploy-offline": "./scripts/deploy_to_offline.sh" + }, + "author": "", + "license": "MIT", + "dependencies": { + "aws-sdk": "^2.469.0" + }, + "devDependencies": { + "aws4": "^1.8.0", + "awscred": "^1.4.2", + "chai": "^4.2.0", + "chai-http": "^4.3.0", + "mocha": "^6.1.4", + "moment": "^2.24.0", + "serverless-dynamodb-local": "^0.2.37", + "ws": "^7.0.0" + } +} diff --git a/manual_test_websocket/manual_test_websocket_authorizer/scripts/deploy_to_aws.sh b/manual_test_websocket/manual_test_websocket_authorizer/scripts/deploy_to_aws.sh new file mode 100755 index 000000000..590bef430 --- /dev/null +++ b/manual_test_websocket/manual_test_websocket_authorizer/scripts/deploy_to_aws.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +echo "Deploying to AWS ..." +echo "Removing node modules ..." +rm -fr ./node_modules +echo "Instaing aws-sdk ..." +npm i aws-sdk +echo "Copying serverless.yml ..." +cp ./scripts/serverless..yml ./serverless.yml +cat ./scripts/serverless.aws.yml >> ./serverless.yml +echo "Deploying to AWS ..." +sls deploy diff --git a/manual_test_websocket/manual_test_websocket_authorizer/scripts/deploy_to_offline.sh b/manual_test_websocket/manual_test_websocket_authorizer/scripts/deploy_to_offline.sh new file mode 100755 index 000000000..adbb643a3 --- /dev/null +++ b/manual_test_websocket/manual_test_websocket_authorizer/scripts/deploy_to_offline.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +echo "Deploying to Offline ..." +echo "Removing node modules ..." +rm -fr ./node_modules +echo "Instaing node modules ..." +npm i +echo "Linking serverless-offline ..." +npm link serverless-offline +echo "Copying serverless.yml ..." +cp ./scripts/serverless..yml ./serverless.yml +cat ./scripts/serverless.offline.yml >> ./serverless.yml +echo "Deploying to Offline ..." +sls offline diff --git a/manual_test_websocket/manual_test_websocket_authorizer/scripts/serverless..yml b/manual_test_websocket/manual_test_websocket_authorizer/scripts/serverless..yml new file mode 100644 index 000000000..78f1b5255 --- /dev/null +++ b/manual_test_websocket/manual_test_websocket_authorizer/scripts/serverless..yml @@ -0,0 +1,35 @@ +# Welcome to Serverless! +# +# This file is the main config file for your service. +# It's very minimal at this point and uses default values. +# You can always add more config options for more control. +# We've included some commented out config examples here. +# Just uncomment any of them to get that config option. +# +# For full config options, check the docs: +# docs.serverless.com +# +# Happy Coding! + +service: manual-test-websocket-authorizer + +provider: + name: aws + runtime: nodejs8.10 + + +functions: + connect: + handler: handler.connect + events: + - websocket: + route: $connect + authorizer: auth + auth: + handler: handler.auth + getClientInfo: + handler: handler.getClientInfo + events: + - websocket: + route: getClientInfo + diff --git a/manual_test_websocket/manual_test_websocket_authorizer/scripts/serverless.aws.yml b/manual_test_websocket/manual_test_websocket_authorizer/scripts/serverless.aws.yml new file mode 100644 index 000000000..64289757a --- /dev/null +++ b/manual_test_websocket/manual_test_websocket_authorizer/scripts/serverless.aws.yml @@ -0,0 +1,9 @@ +plugins: + # - serverless-offline + +package: + exclude: + - ./** + include: + - handler.js + - node_modules/** diff --git a/manual_test_websocket/manual_test_websocket_authorizer/scripts/serverless.offline.yml b/manual_test_websocket/manual_test_websocket_authorizer/scripts/serverless.offline.yml new file mode 100644 index 000000000..bc1edb760 --- /dev/null +++ b/manual_test_websocket/manual_test_websocket_authorizer/scripts/serverless.offline.yml @@ -0,0 +1,4 @@ +plugins: + - serverless-offline + + diff --git a/manual_test_websocket/manual_test_websocket_authorizer/serverless.yml b/manual_test_websocket/manual_test_websocket_authorizer/serverless.yml new file mode 100644 index 000000000..596836955 --- /dev/null +++ b/manual_test_websocket/manual_test_websocket_authorizer/serverless.yml @@ -0,0 +1,7 @@ +###################################### +### DO NOT EDIT THIS FILE DIRECTLY ### +### ### +### User either: ### +### 'npm run deploy-offline' or ### +### 'npm run deploy-aws' ### +###################################### \ No newline at end of file diff --git a/manual_test_websocket/manual_test_websocket_authorizer/test/e2e/ws.e2e.js b/manual_test_websocket/manual_test_websocket_authorizer/test/e2e/ws.e2e.js new file mode 100644 index 000000000..d8db0b706 --- /dev/null +++ b/manual_test_websocket/manual_test_websocket_authorizer/test/e2e/ws.e2e.js @@ -0,0 +1,342 @@ +const chai = require('chai'); +const chaiHttp = require('chai-http'); +chai.use(chaiHttp); +const expect = chai.expect; +const aws4 = require('aws4'); +const awscred = require('awscred'); +const moment = require('moment'); +const endpoint = process.env.npm_config_endpoint||'ws://localhost:3001'; +const timeout = process.env.npm_config_timeout?parseInt(process.env.npm_config_timeout):1000; +const WebSocketTester=require('../support/WebSocketTester'); + +describe('serverless', ()=>{ + describe('with WebSocket support', ()=>{ + let clients=[]; let req=null; let cred=null; + const createWebSocket=async (qs)=>{ + const ws=new WebSocketTester(); + let url=endpoint; + if (qs) url=`${endpoint}?${qs}`; + await ws.open(url); + clients.push(ws); + return ws; + }; + const createClient=async (qs)=>{ + const ws=await createWebSocket(qs); + ws.send(JSON.stringify({action:'getClientInfo'})); + const json=await ws.receive1(); + const id=JSON.parse(json).info.id; + return {ws, id}; + }; + before(async ()=>{ + req=chai.request(`${endpoint.replace('ws://', 'http://').replace('wss://', 'https://')}`).keepOpen(); + // req=chai.request('http://localhost:3001/dev').keepOpen(); + cred=await new Promise((resolve, reject)=>{ + awscred.loadCredentials(function(err, data) { if (err) reject(err); else resolve(data); }); + }); + }); + + beforeEach(()=>{ + clients=[]; + }); + afterEach(async ()=>{ + await Promise.all(clients.map(async (ws, i)=>{ + const n=ws.countUnrecived(); + + if (n>0) { + console.log(`unreceived:[i=${i}]`); + (await ws.receive(n)).forEach(m=>console.log(m)); + } + expect(n).to.equal(0); + ws.close(); + })); + clients=[]; + }); + + it('should request to upgade to WebSocket when receving an HTTP request', async ()=>{ + const req=chai.request(`${endpoint.replace('ws://', 'http://').replace('wss://', 'https://')}`).keepOpen(); + let res=await req.get(`/${Date.now()}`);//.set('Authorization', user.accessToken); + expect(res).to.have.status(426); + res=await req.get(`/${Date.now()}/${Date.now()}`);//.set('Authorization', user.accessToken); + expect(res).to.have.status(426); + }).timeout(timeout); + + it('should open a WebSocket', async ()=>{ + const ws=await createWebSocket(); + expect(ws).not.to.be.undefined; + }).timeout(timeout); + + it('should receive client connection info', async ()=>{ + const ws=await createWebSocket(); + ws.send(JSON.stringify({action:'getClientInfo'})); + const clientInfo=JSON.parse(await ws.receive1()); + expect(clientInfo).to.deep.equal({action:'update', event:'client-info', info:{id:clientInfo.info.id}}); + }).timeout(timeout); + + it('should call default handler when no such action exists', async ()=>{ + const ws=await createWebSocket(); + const payload=JSON.stringify({action:'action'+Date.now()}); + ws.send(payload); + expect(await ws.receive1()).to.equal(`Error: No Supported Action in Payload '${payload}'`); + }).timeout(timeout); + + it('should call default handler when no action provided', async ()=>{ + const ws=await createWebSocket(); + ws.send(JSON.stringify({hello:'world'})); + expect(await ws.receive1()).to.equal(`Error: No Supported Action in Payload '{"hello":"world"}'`); + }).timeout(timeout); + + it('should send & receive data', async ()=>{ + const c1=await createClient(); + const c2=await createClient(); + const c3=await createClient(); + c1.ws.send(JSON.stringify({action:'send', data:'Hello World!', clients:[c1.id, c3.id]})); + expect(await c1.ws.receive1()).to.equal('Hello World!'); + expect(await c3.ws.receive1()).to.equal('Hello World!'); + }).timeout(timeout); + + it('should respond when having an internal server error', async ()=>{ + const conn=await createClient(); + conn.ws.send(JSON.stringify({action:'makeError'})); + const res=JSON.parse(await conn.ws.receive1()); + expect(res).to.deep.equal({message:'Internal server error', connectionId:conn.id, requestId:res.requestId}); + }).timeout(timeout); + + it('should respond via callback', async ()=>{ + const ws=await createWebSocket(); + ws.send(JSON.stringify({action:'replyViaCallback'})); + const res=JSON.parse(await ws.receive1()); + expect(res).to.deep.equal({action:'update', event:'reply-via-callback'}); + }).timeout(timeout); + + it('should respond with error when calling callback(error)', async ()=>{ + const conn=await createClient(); + conn.ws.send(JSON.stringify({action:'replyErrorViaCallback'})); + const res=JSON.parse(await conn.ws.receive1()); + expect(res).to.deep.equal({message:'Internal server error', connectionId:conn.id, requestId:res.requestId}); + }).timeout(timeout); + + it('should respond with only the last action when there are more than one in the serverless.yml file', async ()=>{ + const ws=await createWebSocket(); + ws.send(JSON.stringify({action:'makeMultiCalls'})); + const res=JSON.parse(await ws.receive1()); + expect(res).to.deep.equal({action:'update', event:'made-call-2'}); + }).timeout(timeout); + + it('should not send to non existing client', async ()=>{ + const c1=await createClient(); + c1.ws.send(JSON.stringify({action:'send', data:'Hello World!', clients:["non-existing-id"]})); + expect(await c1.ws.receive1()).to.equal('Error: Could not Send all Messages'); + }).timeout(timeout); + + it('should connect & disconnect', async ()=>{ + const ws=await createWebSocket(); + await ws.send(JSON.stringify({action:'registerListener'})); + await ws.receive1(); + + const c1=await createClient(); + const connect1 = JSON.parse(await ws.receive1()); delete connect1.info.event; delete delete connect1.info.context; + expect(connect1).to.deep.equal({action:'update', event:'connect', info:{id:c1.id}}); + + const c2=await createClient(); + const connect2 = JSON.parse(await ws.receive1()); delete connect2.info.event; delete delete connect2.info.context; + expect(connect2).to.deep.equal({action:'update', event:'connect', info:{id:c2.id}}); + + c2.ws.close(); + const disconnect2 = JSON.parse(await ws.receive1()); delete disconnect2.info.event; delete delete disconnect2.info.context; + expect(disconnect2).to.deep.equal({action:'update', event:'disconnect', info:{id:c2.id}}); + + const c3=await createClient(); + const connect3 = JSON.parse(await ws.receive1()); delete connect3.info.event; delete delete connect3.info.context; + expect(connect3).to.deep.equal({action:'update', event:'connect', info:{id:c3.id}}); + + c1.ws.close(); + const disconnect1 = JSON.parse(await ws.receive1()); delete disconnect1.info.event; delete delete disconnect1.info.context; + expect(disconnect1).to.deep.equal({action:'update', event:'disconnect', info:{id:c1.id}}); + + c3.ws.close(); + const disconnect3 = JSON.parse(await ws.receive1()); delete disconnect3.info.event; delete delete disconnect3.info.context; + expect(disconnect3).to.deep.equal({action:'update', event:'disconnect', info:{id:c3.id}}); + }).timeout(timeout); + + const createExpectedEvent=(connectionId, action, eventType, actualEvent)=>{ + const url=new URL(endpoint); + const expected={ + apiGatewayUrl: `${actualEvent.apiGatewayUrl}`, + isBase64Encoded: false, + requestContext: { + apiId: actualEvent.requestContext.apiId, + connectedAt: actualEvent.requestContext.connectedAt, + connectionId: `${connectionId}`, + domainName: url.hostname, + eventType, + extendedRequestId: actualEvent.requestContext.extendedRequestId, + identity: { + accessKey: null, + accountId: null, + caller: null, + cognitoAuthenticationProvider: null, + cognitoAuthenticationType: null, + cognitoIdentityId: null, + cognitoIdentityPoolId: null, + principalOrgId: null, + sourceIp: actualEvent.requestContext.identity.sourceIp, + user: null, + userAgent: null, + userArn: null, + }, + messageDirection: 'IN', + messageId: actualEvent.requestContext.messageId, + requestId: actualEvent.requestContext.requestId, + requestTime: actualEvent.requestContext.requestTime, + requestTimeEpoch: actualEvent.requestContext.requestTimeEpoch, + routeKey: action, + stage: actualEvent.requestContext.stage, + }, + }; + + return expected; + }; + + const createExpectedContext=(actualContext)=>{ + const expected={ + awsRequestId: actualContext.awsRequestId, + callbackWaitsForEmptyEventLoop: true, + functionName: actualContext.functionName, + functionVersion: '$LATEST', + invokedFunctionArn: actualContext.invokedFunctionArn, + invokeid: actualContext.invokeid, + logGroupName: actualContext.logGroupName, + logStreamName: actualContext.logStreamName, + memoryLimitInMB: actualContext.memoryLimitInMB, + }; + + return expected; + }; + + const createExpectedConnectHeaders=(actualHeaders)=>{ + const url=new URL(endpoint); + const expected={ + Host: url.hostname, + 'Sec-WebSocket-Extensions': actualHeaders['Sec-WebSocket-Extensions'], + 'Sec-WebSocket-Key': actualHeaders['Sec-WebSocket-Key'], + 'Sec-WebSocket-Version': actualHeaders['Sec-WebSocket-Version'], + 'X-Amzn-Trace-Id': actualHeaders['X-Amzn-Trace-Id'], + 'X-Forwarded-For': actualHeaders['X-Forwarded-For'], + 'X-Forwarded-Port': `${url.port||443}`, + 'X-Forwarded-Proto': `${url.protocol.replace('ws', 'http').replace('wss', 'https').replace(':', '')}` + }; + + return expected; + }; + + const createExpectedDisconnectHeaders=(actualHeaders)=>{ + const url=new URL(endpoint); + const expected={ + Host: url.hostname, + 'x-api-key': '', + 'x-restapi': '', + }; + + return expected; + }; + + const createExpectedConnectMultiValueHeaders=(actualHeaders)=>{ + const expected=createExpectedConnectHeaders(actualHeaders); + Object.keys(expected).map((key, index)=>{ + expected[key] = [expected[key]]; + }); + return expected; + }; + + const createExpectedDisconnectMultiValueHeaders=(actualHeaders)=>{ + const expected=createExpectedDisconnectHeaders(actualHeaders); + Object.keys(expected).map((key, index)=>{ + expected[key] = [expected[key]]; + }); + return expected; + }; + + it('should receive correct call info', async ()=>{ + const ws=await createWebSocket(); + await ws.send(JSON.stringify({action:'registerListener'})); + await ws.receive1(); + + // connect + const c=await createClient(); + const connect=JSON.parse(await ws.receive1()); + let now=Date.now(); + let expectedCallInfo={id:c.id, event:{headers:createExpectedConnectHeaders(connect.info.event.headers), multiValueHeaders:createExpectedConnectMultiValueHeaders(connect.info.event.headers), ...createExpectedEvent(c.id, '$connect', 'CONNECT', connect.info.event)}, context:createExpectedContext(connect.info.context)}; + expect(connect).to.deep.equal({action:'update', event:'connect', info:expectedCallInfo}); + expect(connect.info.event.requestContext.requestTimeEpoch).to.be.within(connect.info.event.requestContext.connectedAt-10, connect.info.event.requestContext.requestTimeEpoch+10); + expect(connect.info.event.requestContext.connectedAt).to.be.within(now-timeout, now); + expect(connect.info.event.requestContext.requestTimeEpoch).to.be.within(now-timeout, now); + expect(moment.utc(connect.info.event.requestContext.requestTime, 'D/MMM/YYYY:H:m:s Z').toDate().getTime()).to.be.within(now-timeout, now); + if (endpoint.startsWith('ws://locahost')) { + expect(connect.info.event.apiGatewayUrl).to.equal(endpoint.replace('ws://', 'http://').replace('wss://', 'https://')); + expect(connect.info.event.headers['X-Forwarded-For']).to.be.equal('127.0.0.1'); + } + + // getCallInfo + c.ws.send(JSON.stringify({action:'getCallInfo'})); + const callInfo=JSON.parse(await c.ws.receive1()); + now=Date.now(); + expectedCallInfo={event:{body: '{\"action\":\"getCallInfo\"}', ...createExpectedEvent(c.id, 'getCallInfo', 'MESSAGE', callInfo.info.event)}, context:createExpectedContext(callInfo.info.context)}; + expect(callInfo).to.deep.equal({action:'update', event:'call-info', info:expectedCallInfo}); + expect(callInfo.info.event.requestContext.connectedAt).to.be.lt(callInfo.info.event.requestContext.requestTimeEpoch); + expect(callInfo.info.event.requestContext.connectedAt).to.be.within(now-timeout, now); + expect(callInfo.info.event.requestContext.requestTimeEpoch).to.be.within(now-timeout, now); + expect(moment.utc(callInfo.info.event.requestContext.requestTime, 'D/MMM/YYYY:H:m:s Z').toDate().getTime()).to.be.within(now-timeout, now); + if (endpoint.startsWith('ws://locahost')) expect(callInfo.info.event.apiGatewayUrl).to.equal(endpoint.replace('ws://', 'http://').replace('wss://', 'https://')); + + // disconnect + c.ws.close(); + const disconnect=JSON.parse(await ws.receive1()); + now=Date.now(); + expectedCallInfo={id:c.id, event:{headers:createExpectedDisconnectHeaders(disconnect.info.event.headers), multiValueHeaders:createExpectedDisconnectMultiValueHeaders(disconnect.info.event.headers), ...createExpectedEvent(c.id, '$disconnect', 'DISCONNECT', disconnect.info.event)}, context:createExpectedContext(disconnect.info.context)}; + expect(disconnect).to.deep.equal({action:'update', event:'disconnect', info:expectedCallInfo}); + }).timeout(timeout); + + it('should be able to parse query string', async ()=>{ + const now=''+Date.now(); + const ws=await createWebSocket(); + await ws.send(JSON.stringify({action:'registerListener'})); + await ws.receive1(); + + const c1=await createClient(); + const c2=await createClient(`now=${now}&before=123456789`); + expect(JSON.parse(await ws.receive1()).info.event.queryStringParameters).to.be.undefined; + expect(JSON.parse(await ws.receive1()).info.event.queryStringParameters).to.deep.equal({now, before:'123456789'}); + }).timeout(timeout); + + it('should be able to receive messages via REST API', async ()=>{ + const c1=await createClient(); + const c2=await createClient(); + const url=new URL(endpoint); + const signature = {service: 'execute-api', host:url.host, path:`${url.pathname}/@connections/${c2.id}`, method: 'POST', body:'Hello World!', headers:{'Content-Type':'text/plain'/*'application/text'*/}}; + aws4.sign(signature, {accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey}); + const res=await req.post(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers['Authorization']).set('Content-Type', signature.headers['Content-Type']).send('Hello World!'); + expect(res).to.have.status(200); + expect(await c2.ws.receive1()).to.equal('Hello World!'); + }).timeout(timeout); + + it('should receive error code when sending to non existing client via REST API', async ()=>{ + const c='aJz0Md6VoAMCIbQ='; + const url=new URL(endpoint); + const signature = {service: 'execute-api', host:url.host, path:`${url.pathname}/@connections/${c}`, method: 'POST', body:'Hello World!', headers:{'Content-Type':'text/plain'/*'application/text'*/}}; + aws4.sign(signature, {accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey}); + const res=await req.post(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers['Authorization']).set('Content-Type', signature.headers['Content-Type']).send('Hello World!'); + expect(res).to.have.status(410); + }).timeout(timeout); + + // UNABLE TO TEST HIS SCENARIO BECAUSE AWS DOESN'T RETURN ANYTHING + // it('should not receive anything when POSTing nothing', async ()=>{ + // const c1=await createClient(); + // const url=new URL(endpoint); + // const signature = {service: 'execute-api', host:url.host, path:`${url.pathname}/@connections/${c1.id}`, method: 'POST'/*, body:'Hello World!'*/, headers:{'Content-Type':'text/plain'/*'application/text'*/}}; + // aws4.sign(signature, {accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey}); + // const res=await req.post(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers['Authorization']).set('Content-Type', signature.headers['Content-Type']).send(/*'Hello World!'*/); + // expect(res).to.have.status(200); + // }).timeout(timeout); + + }); +}); \ No newline at end of file diff --git a/manual_test_websocket/manual_test_websocket_authorizer/test/support/WebSocketTester.js b/manual_test_websocket/manual_test_websocket_authorizer/test/support/WebSocketTester.js new file mode 100644 index 000000000..ee5c8d6fb --- /dev/null +++ b/manual_test_websocket/manual_test_websocket_authorizer/test/support/WebSocketTester.js @@ -0,0 +1,60 @@ +const WebSocket = require('ws'); + +class WebSocketTester { + constructor() { + this.messages=[]; this.receivers=[]; + } + + open(url) { + if (null!=this.ws) return; + const ws=this.ws=new WebSocket(url); + ws.on('message', (message)=>{ + // console.log('Received: '+message); + if (0 { + ws.on('open', ()=>{ + resolve(true); + }); + }); + } + + send(data) { + this.ws.send(data); + } + + receive1() { + return new Promise((resolve/*, reject*/)=>{ + if (0{ + const messages=[]; + for (let i=0; i{ + messages[i]=message; + if (i===n-1) resolve(messages); + }); + } + }); + } + + skip() { + if (0{}); + } + + countUnrecived() { + return this.messages.length; + } + + close() { + if (null!=this.ws) this.ws.close(); + } +}; + +module.exports=WebSocketTester; \ No newline at end of file From 02ce0e7b38a926d7cb2f5f39ef5d731a868711c5 Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Tue, 11 Jun 2019 23:19:58 +0300 Subject: [PATCH 25/71] Support for websocketsApiRouteSelectionExpression In the form of $request.body.x.y.z --- .../handler.js | 37 ++ .../package-lock.json | 6 +- .../package.json | 2 +- .../scripts/deploy_to_aws.sh | 0 .../scripts/deploy_to_offline.sh | 0 .../scripts/serverless..yml | 21 +- .../scripts/serverless.aws.yml | 0 .../scripts/serverless.offline.yml | 7 + .../serverless.yml | 0 .../test/e2e/ws.e2e.js | 53 +++ .../test/support/WebSocketTester.js | 0 .../handler.js | 60 --- .../scripts/serverless.offline.yml | 4 - .../test/e2e/ws.e2e.js | 342 ------------------ src/index.js | 13 +- 15 files changed, 120 insertions(+), 425 deletions(-) create mode 100644 manual_test_websocket/manual_test_websocket_RouteSelection/handler.js rename manual_test_websocket/{manual_test_websocket_authorizer => manual_test_websocket_RouteSelection}/package-lock.json (99%) rename manual_test_websocket/{manual_test_websocket_authorizer => manual_test_websocket_RouteSelection}/package.json (95%) rename manual_test_websocket/{manual_test_websocket_authorizer => manual_test_websocket_RouteSelection}/scripts/deploy_to_aws.sh (100%) rename manual_test_websocket/{manual_test_websocket_authorizer => manual_test_websocket_RouteSelection}/scripts/deploy_to_offline.sh (100%) rename manual_test_websocket/{manual_test_websocket_authorizer => manual_test_websocket_RouteSelection}/scripts/serverless..yml (61%) rename manual_test_websocket/{manual_test_websocket_authorizer => manual_test_websocket_RouteSelection}/scripts/serverless.aws.yml (100%) create mode 100644 manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless.offline.yml rename manual_test_websocket/{manual_test_websocket_authorizer => manual_test_websocket_RouteSelection}/serverless.yml (100%) create mode 100644 manual_test_websocket/manual_test_websocket_RouteSelection/test/e2e/ws.e2e.js rename manual_test_websocket/{manual_test_websocket_authorizer => manual_test_websocket_RouteSelection}/test/support/WebSocketTester.js (100%) delete mode 100644 manual_test_websocket/manual_test_websocket_authorizer/handler.js delete mode 100644 manual_test_websocket/manual_test_websocket_authorizer/scripts/serverless.offline.yml delete mode 100644 manual_test_websocket/manual_test_websocket_authorizer/test/e2e/ws.e2e.js diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/handler.js b/manual_test_websocket/manual_test_websocket_RouteSelection/handler.js new file mode 100644 index 000000000..e8da78fd7 --- /dev/null +++ b/manual_test_websocket/manual_test_websocket_RouteSelection/handler.js @@ -0,0 +1,37 @@ +'use strict'; + +const AWS = require('aws-sdk'); + + +const successfullResponse = { + statusCode: 200, + body: 'Request is OK.' +}; + +const errorResponse = { + statusCode: 400, + body: 'Request is not OK.' +}; + +module.exports.echo = async (event, context) => { + const action = JSON.parse(event.body); + + await sendToClient(action.message, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)); + return successfullResponse; +}; + +const newAWSApiGatewayManagementApi=(event, context)=>{ + let endpoint=event.apiGatewayUrl; + + if (!endpoint) endpoint = event.requestContext.domainName+'/'+event.requestContext.stage; + const apiVersion='2018-11-29'; + return new AWS.ApiGatewayManagementApi({ apiVersion, endpoint }); +}; + +const sendToClient = (data, connectionId, apigwManagementApi) => { + // console.log(`sendToClient:${connectionId}`); + let sendee=data; + if ('object'==typeof data) sendee=JSON.stringify(data); + + return apigwManagementApi.postToConnection({ConnectionId: connectionId, Data: sendee}).promise(); +}; diff --git a/manual_test_websocket/manual_test_websocket_authorizer/package-lock.json b/manual_test_websocket/manual_test_websocket_RouteSelection/package-lock.json similarity index 99% rename from manual_test_websocket/manual_test_websocket_authorizer/package-lock.json rename to manual_test_websocket/manual_test_websocket_RouteSelection/package-lock.json index 4c445744f..5316e3b25 100644 --- a/manual_test_websocket/manual_test_websocket_authorizer/package-lock.json +++ b/manual_test_websocket/manual_test_websocket_RouteSelection/package-lock.json @@ -81,9 +81,9 @@ "dev": true }, "aws-sdk": { - "version": "2.469.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.469.0.tgz", - "integrity": "sha512-VaIrO3aBX83gKkBPk9xM0RHmu7fmq76kaF0SqbsWlPImgxc5foJ4rBlRMMlmeNogFZZ/XTQdI+gkFDVosV14Ig==", + "version": "2.472.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.472.0.tgz", + "integrity": "sha512-uFatrjfMSwC34VxdG9ollX6K61e+iaoE5ZHQ/OKeSoWx9HXs3AwJqIS90iBLEhaAm2WoTMFYAv0irMC8eMCu3g==", "requires": { "buffer": "4.9.1", "events": "1.1.1", diff --git a/manual_test_websocket/manual_test_websocket_authorizer/package.json b/manual_test_websocket/manual_test_websocket_RouteSelection/package.json similarity index 95% rename from manual_test_websocket/manual_test_websocket_authorizer/package.json rename to manual_test_websocket/manual_test_websocket_RouteSelection/package.json index 2d492794a..d580cdaa4 100644 --- a/manual_test_websocket/manual_test_websocket_authorizer/package.json +++ b/manual_test_websocket/manual_test_websocket_RouteSelection/package.json @@ -13,7 +13,7 @@ "author": "", "license": "MIT", "dependencies": { - "aws-sdk": "^2.469.0" + "aws-sdk": "^2.472.0" }, "devDependencies": { "aws4": "^1.8.0", diff --git a/manual_test_websocket/manual_test_websocket_authorizer/scripts/deploy_to_aws.sh b/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/deploy_to_aws.sh similarity index 100% rename from manual_test_websocket/manual_test_websocket_authorizer/scripts/deploy_to_aws.sh rename to manual_test_websocket/manual_test_websocket_RouteSelection/scripts/deploy_to_aws.sh diff --git a/manual_test_websocket/manual_test_websocket_authorizer/scripts/deploy_to_offline.sh b/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/deploy_to_offline.sh similarity index 100% rename from manual_test_websocket/manual_test_websocket_authorizer/scripts/deploy_to_offline.sh rename to manual_test_websocket/manual_test_websocket_RouteSelection/scripts/deploy_to_offline.sh diff --git a/manual_test_websocket/manual_test_websocket_authorizer/scripts/serverless..yml b/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless..yml similarity index 61% rename from manual_test_websocket/manual_test_websocket_authorizer/scripts/serverless..yml rename to manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless..yml index 78f1b5255..0de4ad4e9 100644 --- a/manual_test_websocket/manual_test_websocket_authorizer/scripts/serverless..yml +++ b/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless..yml @@ -11,25 +11,18 @@ # # Happy Coding! -service: manual-test-websocket-authorizer +service: manual-test-websocket-RouteSelection provider: name: aws runtime: nodejs8.10 + websocketsApiRouteSelectionExpression: $request.body.service.do -functions: - connect: - handler: handler.connect +functions: + echo: + handler: handler.echo events: - websocket: - route: $connect - authorizer: auth - auth: - handler: handler.auth - getClientInfo: - handler: handler.getClientInfo - events: - - websocket: - route: getClientInfo - + route: echo + diff --git a/manual_test_websocket/manual_test_websocket_authorizer/scripts/serverless.aws.yml b/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless.aws.yml similarity index 100% rename from manual_test_websocket/manual_test_websocket_authorizer/scripts/serverless.aws.yml rename to manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless.aws.yml diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless.offline.yml b/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless.offline.yml new file mode 100644 index 000000000..b3bcbfa71 --- /dev/null +++ b/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless.offline.yml @@ -0,0 +1,7 @@ +plugins: + - serverless-offline + +custom: + serverless-offline: + port: 3004 + diff --git a/manual_test_websocket/manual_test_websocket_authorizer/serverless.yml b/manual_test_websocket/manual_test_websocket_RouteSelection/serverless.yml similarity index 100% rename from manual_test_websocket/manual_test_websocket_authorizer/serverless.yml rename to manual_test_websocket/manual_test_websocket_RouteSelection/serverless.yml diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/test/e2e/ws.e2e.js b/manual_test_websocket/manual_test_websocket_RouteSelection/test/e2e/ws.e2e.js new file mode 100644 index 000000000..a5659364d --- /dev/null +++ b/manual_test_websocket/manual_test_websocket_RouteSelection/test/e2e/ws.e2e.js @@ -0,0 +1,53 @@ +const chai = require('chai'); +const expect = chai.expect; +const endpoint = process.env.npm_config_endpoint||'ws://localhost:3005'; +const timeout = process.env.npm_config_timeout?parseInt(process.env.npm_config_timeout):1000; +const WebSocketTester=require('../support/WebSocketTester'); + +describe('serverless', ()=>{ + describe('with WebSocket support', ()=>{ + let clients=[]; let req=null; let cred=null; + const createWebSocket=async (qs)=>{ + const ws=new WebSocketTester(); + let url=endpoint; + if (qs) url=`${endpoint}?${qs}`; + await ws.open(url); + clients.push(ws); + return ws; + }; + const createClient=async (qs)=>{ + const ws=await createWebSocket(qs); + ws.send(JSON.stringify({action:'getClientInfo'})); + const json=await ws.receive1(); + const id=JSON.parse(json).info.id; + return {ws, id}; + }; + + beforeEach(()=>{ + clients=[]; + }); + afterEach(async ()=>{ + await Promise.all(clients.map(async (ws, i)=>{ + const n=ws.countUnrecived(); + + if (n>0) { + console.log(`unreceived:[i=${i}]`); + (await ws.receive(n)).forEach(m=>console.log(m)); + } + expect(n).to.equal(0); + ws.close(); + })); + clients=[]; + }); + + it(`should call action 'echo' handler located at service.do`, async ()=>{ + const ws=await createWebSocket(); + const now=""+Date.now(); + const payload=JSON.stringify({service:{do:'echo'}, message:now}); + ws.send(payload); + expect(await ws.receive1()).to.equal(`${now}`); + }).timeout(timeout); + + + }); +}); \ No newline at end of file diff --git a/manual_test_websocket/manual_test_websocket_authorizer/test/support/WebSocketTester.js b/manual_test_websocket/manual_test_websocket_RouteSelection/test/support/WebSocketTester.js similarity index 100% rename from manual_test_websocket/manual_test_websocket_authorizer/test/support/WebSocketTester.js rename to manual_test_websocket/manual_test_websocket_RouteSelection/test/support/WebSocketTester.js diff --git a/manual_test_websocket/manual_test_websocket_authorizer/handler.js b/manual_test_websocket/manual_test_websocket_authorizer/handler.js deleted file mode 100644 index b433807be..000000000 --- a/manual_test_websocket/manual_test_websocket_authorizer/handler.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict'; - -const AWS = require('aws-sdk'); - -const successfullResponse = { - statusCode: 200, - body: 'Request is OK.' -}; - -const errorResponse = { - statusCode: 400, - body: 'Request is not OK.' -}; - -const generatePolicy = function(principalId, effect, resource) { - const authResponse = {}; - authResponse.principalId = principalId; - if (effect && resource) { - const policyDocument = {}; - policyDocument.Version = '2012-10-17'; - policyDocument.Statement = []; - const statementOne = {}; - statementOne.Action = 'execute-api:Invoke'; - statementOne.Effect = effect; - statementOne.Resource = resource; - policyDocument.Statement[0] = statementOne; - authResponse.policyDocument = policyDocument; - } - return authResponse; -}; - - -module.exports.connect = async (event, context) => { - // console.log('connect:'); - return successfullResponse; -}; - -module.export.auth = (event, context, callback) => { - //console.log('auth:'); - const token = event.headers["Authorization"]; - - if ('deny'===token) callback(null, generatePolicy('user', 'Deny', event.methodArn)); - else callback(null, generatePolicy('user', 'Allow', event.methodArn));; -}; - -const newAWSApiGatewayManagementApi=(event, context)=>{ - let endpoint=event.apiGatewayUrl; - - if (!endpoint) endpoint = event.requestContext.domainName+'/'+event.requestContext.stage; - const apiVersion='2018-11-29'; - return new AWS.ApiGatewayManagementApi({ apiVersion, endpoint }); -}; - -const sendToClient = (data, connectionId, apigwManagementApi) => { - // console.log(`sendToClient:${connectionId}`); - let sendee=data; - if ('object'==typeof data) sendee=JSON.stringify(data); - - return apigwManagementApi.postToConnection({ConnectionId: connectionId, Data: sendee}).promise(); -}; diff --git a/manual_test_websocket/manual_test_websocket_authorizer/scripts/serverless.offline.yml b/manual_test_websocket/manual_test_websocket_authorizer/scripts/serverless.offline.yml deleted file mode 100644 index bc1edb760..000000000 --- a/manual_test_websocket/manual_test_websocket_authorizer/scripts/serverless.offline.yml +++ /dev/null @@ -1,4 +0,0 @@ -plugins: - - serverless-offline - - diff --git a/manual_test_websocket/manual_test_websocket_authorizer/test/e2e/ws.e2e.js b/manual_test_websocket/manual_test_websocket_authorizer/test/e2e/ws.e2e.js deleted file mode 100644 index d8db0b706..000000000 --- a/manual_test_websocket/manual_test_websocket_authorizer/test/e2e/ws.e2e.js +++ /dev/null @@ -1,342 +0,0 @@ -const chai = require('chai'); -const chaiHttp = require('chai-http'); -chai.use(chaiHttp); -const expect = chai.expect; -const aws4 = require('aws4'); -const awscred = require('awscred'); -const moment = require('moment'); -const endpoint = process.env.npm_config_endpoint||'ws://localhost:3001'; -const timeout = process.env.npm_config_timeout?parseInt(process.env.npm_config_timeout):1000; -const WebSocketTester=require('../support/WebSocketTester'); - -describe('serverless', ()=>{ - describe('with WebSocket support', ()=>{ - let clients=[]; let req=null; let cred=null; - const createWebSocket=async (qs)=>{ - const ws=new WebSocketTester(); - let url=endpoint; - if (qs) url=`${endpoint}?${qs}`; - await ws.open(url); - clients.push(ws); - return ws; - }; - const createClient=async (qs)=>{ - const ws=await createWebSocket(qs); - ws.send(JSON.stringify({action:'getClientInfo'})); - const json=await ws.receive1(); - const id=JSON.parse(json).info.id; - return {ws, id}; - }; - before(async ()=>{ - req=chai.request(`${endpoint.replace('ws://', 'http://').replace('wss://', 'https://')}`).keepOpen(); - // req=chai.request('http://localhost:3001/dev').keepOpen(); - cred=await new Promise((resolve, reject)=>{ - awscred.loadCredentials(function(err, data) { if (err) reject(err); else resolve(data); }); - }); - }); - - beforeEach(()=>{ - clients=[]; - }); - afterEach(async ()=>{ - await Promise.all(clients.map(async (ws, i)=>{ - const n=ws.countUnrecived(); - - if (n>0) { - console.log(`unreceived:[i=${i}]`); - (await ws.receive(n)).forEach(m=>console.log(m)); - } - expect(n).to.equal(0); - ws.close(); - })); - clients=[]; - }); - - it('should request to upgade to WebSocket when receving an HTTP request', async ()=>{ - const req=chai.request(`${endpoint.replace('ws://', 'http://').replace('wss://', 'https://')}`).keepOpen(); - let res=await req.get(`/${Date.now()}`);//.set('Authorization', user.accessToken); - expect(res).to.have.status(426); - res=await req.get(`/${Date.now()}/${Date.now()}`);//.set('Authorization', user.accessToken); - expect(res).to.have.status(426); - }).timeout(timeout); - - it('should open a WebSocket', async ()=>{ - const ws=await createWebSocket(); - expect(ws).not.to.be.undefined; - }).timeout(timeout); - - it('should receive client connection info', async ()=>{ - const ws=await createWebSocket(); - ws.send(JSON.stringify({action:'getClientInfo'})); - const clientInfo=JSON.parse(await ws.receive1()); - expect(clientInfo).to.deep.equal({action:'update', event:'client-info', info:{id:clientInfo.info.id}}); - }).timeout(timeout); - - it('should call default handler when no such action exists', async ()=>{ - const ws=await createWebSocket(); - const payload=JSON.stringify({action:'action'+Date.now()}); - ws.send(payload); - expect(await ws.receive1()).to.equal(`Error: No Supported Action in Payload '${payload}'`); - }).timeout(timeout); - - it('should call default handler when no action provided', async ()=>{ - const ws=await createWebSocket(); - ws.send(JSON.stringify({hello:'world'})); - expect(await ws.receive1()).to.equal(`Error: No Supported Action in Payload '{"hello":"world"}'`); - }).timeout(timeout); - - it('should send & receive data', async ()=>{ - const c1=await createClient(); - const c2=await createClient(); - const c3=await createClient(); - c1.ws.send(JSON.stringify({action:'send', data:'Hello World!', clients:[c1.id, c3.id]})); - expect(await c1.ws.receive1()).to.equal('Hello World!'); - expect(await c3.ws.receive1()).to.equal('Hello World!'); - }).timeout(timeout); - - it('should respond when having an internal server error', async ()=>{ - const conn=await createClient(); - conn.ws.send(JSON.stringify({action:'makeError'})); - const res=JSON.parse(await conn.ws.receive1()); - expect(res).to.deep.equal({message:'Internal server error', connectionId:conn.id, requestId:res.requestId}); - }).timeout(timeout); - - it('should respond via callback', async ()=>{ - const ws=await createWebSocket(); - ws.send(JSON.stringify({action:'replyViaCallback'})); - const res=JSON.parse(await ws.receive1()); - expect(res).to.deep.equal({action:'update', event:'reply-via-callback'}); - }).timeout(timeout); - - it('should respond with error when calling callback(error)', async ()=>{ - const conn=await createClient(); - conn.ws.send(JSON.stringify({action:'replyErrorViaCallback'})); - const res=JSON.parse(await conn.ws.receive1()); - expect(res).to.deep.equal({message:'Internal server error', connectionId:conn.id, requestId:res.requestId}); - }).timeout(timeout); - - it('should respond with only the last action when there are more than one in the serverless.yml file', async ()=>{ - const ws=await createWebSocket(); - ws.send(JSON.stringify({action:'makeMultiCalls'})); - const res=JSON.parse(await ws.receive1()); - expect(res).to.deep.equal({action:'update', event:'made-call-2'}); - }).timeout(timeout); - - it('should not send to non existing client', async ()=>{ - const c1=await createClient(); - c1.ws.send(JSON.stringify({action:'send', data:'Hello World!', clients:["non-existing-id"]})); - expect(await c1.ws.receive1()).to.equal('Error: Could not Send all Messages'); - }).timeout(timeout); - - it('should connect & disconnect', async ()=>{ - const ws=await createWebSocket(); - await ws.send(JSON.stringify({action:'registerListener'})); - await ws.receive1(); - - const c1=await createClient(); - const connect1 = JSON.parse(await ws.receive1()); delete connect1.info.event; delete delete connect1.info.context; - expect(connect1).to.deep.equal({action:'update', event:'connect', info:{id:c1.id}}); - - const c2=await createClient(); - const connect2 = JSON.parse(await ws.receive1()); delete connect2.info.event; delete delete connect2.info.context; - expect(connect2).to.deep.equal({action:'update', event:'connect', info:{id:c2.id}}); - - c2.ws.close(); - const disconnect2 = JSON.parse(await ws.receive1()); delete disconnect2.info.event; delete delete disconnect2.info.context; - expect(disconnect2).to.deep.equal({action:'update', event:'disconnect', info:{id:c2.id}}); - - const c3=await createClient(); - const connect3 = JSON.parse(await ws.receive1()); delete connect3.info.event; delete delete connect3.info.context; - expect(connect3).to.deep.equal({action:'update', event:'connect', info:{id:c3.id}}); - - c1.ws.close(); - const disconnect1 = JSON.parse(await ws.receive1()); delete disconnect1.info.event; delete delete disconnect1.info.context; - expect(disconnect1).to.deep.equal({action:'update', event:'disconnect', info:{id:c1.id}}); - - c3.ws.close(); - const disconnect3 = JSON.parse(await ws.receive1()); delete disconnect3.info.event; delete delete disconnect3.info.context; - expect(disconnect3).to.deep.equal({action:'update', event:'disconnect', info:{id:c3.id}}); - }).timeout(timeout); - - const createExpectedEvent=(connectionId, action, eventType, actualEvent)=>{ - const url=new URL(endpoint); - const expected={ - apiGatewayUrl: `${actualEvent.apiGatewayUrl}`, - isBase64Encoded: false, - requestContext: { - apiId: actualEvent.requestContext.apiId, - connectedAt: actualEvent.requestContext.connectedAt, - connectionId: `${connectionId}`, - domainName: url.hostname, - eventType, - extendedRequestId: actualEvent.requestContext.extendedRequestId, - identity: { - accessKey: null, - accountId: null, - caller: null, - cognitoAuthenticationProvider: null, - cognitoAuthenticationType: null, - cognitoIdentityId: null, - cognitoIdentityPoolId: null, - principalOrgId: null, - sourceIp: actualEvent.requestContext.identity.sourceIp, - user: null, - userAgent: null, - userArn: null, - }, - messageDirection: 'IN', - messageId: actualEvent.requestContext.messageId, - requestId: actualEvent.requestContext.requestId, - requestTime: actualEvent.requestContext.requestTime, - requestTimeEpoch: actualEvent.requestContext.requestTimeEpoch, - routeKey: action, - stage: actualEvent.requestContext.stage, - }, - }; - - return expected; - }; - - const createExpectedContext=(actualContext)=>{ - const expected={ - awsRequestId: actualContext.awsRequestId, - callbackWaitsForEmptyEventLoop: true, - functionName: actualContext.functionName, - functionVersion: '$LATEST', - invokedFunctionArn: actualContext.invokedFunctionArn, - invokeid: actualContext.invokeid, - logGroupName: actualContext.logGroupName, - logStreamName: actualContext.logStreamName, - memoryLimitInMB: actualContext.memoryLimitInMB, - }; - - return expected; - }; - - const createExpectedConnectHeaders=(actualHeaders)=>{ - const url=new URL(endpoint); - const expected={ - Host: url.hostname, - 'Sec-WebSocket-Extensions': actualHeaders['Sec-WebSocket-Extensions'], - 'Sec-WebSocket-Key': actualHeaders['Sec-WebSocket-Key'], - 'Sec-WebSocket-Version': actualHeaders['Sec-WebSocket-Version'], - 'X-Amzn-Trace-Id': actualHeaders['X-Amzn-Trace-Id'], - 'X-Forwarded-For': actualHeaders['X-Forwarded-For'], - 'X-Forwarded-Port': `${url.port||443}`, - 'X-Forwarded-Proto': `${url.protocol.replace('ws', 'http').replace('wss', 'https').replace(':', '')}` - }; - - return expected; - }; - - const createExpectedDisconnectHeaders=(actualHeaders)=>{ - const url=new URL(endpoint); - const expected={ - Host: url.hostname, - 'x-api-key': '', - 'x-restapi': '', - }; - - return expected; - }; - - const createExpectedConnectMultiValueHeaders=(actualHeaders)=>{ - const expected=createExpectedConnectHeaders(actualHeaders); - Object.keys(expected).map((key, index)=>{ - expected[key] = [expected[key]]; - }); - return expected; - }; - - const createExpectedDisconnectMultiValueHeaders=(actualHeaders)=>{ - const expected=createExpectedDisconnectHeaders(actualHeaders); - Object.keys(expected).map((key, index)=>{ - expected[key] = [expected[key]]; - }); - return expected; - }; - - it('should receive correct call info', async ()=>{ - const ws=await createWebSocket(); - await ws.send(JSON.stringify({action:'registerListener'})); - await ws.receive1(); - - // connect - const c=await createClient(); - const connect=JSON.parse(await ws.receive1()); - let now=Date.now(); - let expectedCallInfo={id:c.id, event:{headers:createExpectedConnectHeaders(connect.info.event.headers), multiValueHeaders:createExpectedConnectMultiValueHeaders(connect.info.event.headers), ...createExpectedEvent(c.id, '$connect', 'CONNECT', connect.info.event)}, context:createExpectedContext(connect.info.context)}; - expect(connect).to.deep.equal({action:'update', event:'connect', info:expectedCallInfo}); - expect(connect.info.event.requestContext.requestTimeEpoch).to.be.within(connect.info.event.requestContext.connectedAt-10, connect.info.event.requestContext.requestTimeEpoch+10); - expect(connect.info.event.requestContext.connectedAt).to.be.within(now-timeout, now); - expect(connect.info.event.requestContext.requestTimeEpoch).to.be.within(now-timeout, now); - expect(moment.utc(connect.info.event.requestContext.requestTime, 'D/MMM/YYYY:H:m:s Z').toDate().getTime()).to.be.within(now-timeout, now); - if (endpoint.startsWith('ws://locahost')) { - expect(connect.info.event.apiGatewayUrl).to.equal(endpoint.replace('ws://', 'http://').replace('wss://', 'https://')); - expect(connect.info.event.headers['X-Forwarded-For']).to.be.equal('127.0.0.1'); - } - - // getCallInfo - c.ws.send(JSON.stringify({action:'getCallInfo'})); - const callInfo=JSON.parse(await c.ws.receive1()); - now=Date.now(); - expectedCallInfo={event:{body: '{\"action\":\"getCallInfo\"}', ...createExpectedEvent(c.id, 'getCallInfo', 'MESSAGE', callInfo.info.event)}, context:createExpectedContext(callInfo.info.context)}; - expect(callInfo).to.deep.equal({action:'update', event:'call-info', info:expectedCallInfo}); - expect(callInfo.info.event.requestContext.connectedAt).to.be.lt(callInfo.info.event.requestContext.requestTimeEpoch); - expect(callInfo.info.event.requestContext.connectedAt).to.be.within(now-timeout, now); - expect(callInfo.info.event.requestContext.requestTimeEpoch).to.be.within(now-timeout, now); - expect(moment.utc(callInfo.info.event.requestContext.requestTime, 'D/MMM/YYYY:H:m:s Z').toDate().getTime()).to.be.within(now-timeout, now); - if (endpoint.startsWith('ws://locahost')) expect(callInfo.info.event.apiGatewayUrl).to.equal(endpoint.replace('ws://', 'http://').replace('wss://', 'https://')); - - // disconnect - c.ws.close(); - const disconnect=JSON.parse(await ws.receive1()); - now=Date.now(); - expectedCallInfo={id:c.id, event:{headers:createExpectedDisconnectHeaders(disconnect.info.event.headers), multiValueHeaders:createExpectedDisconnectMultiValueHeaders(disconnect.info.event.headers), ...createExpectedEvent(c.id, '$disconnect', 'DISCONNECT', disconnect.info.event)}, context:createExpectedContext(disconnect.info.context)}; - expect(disconnect).to.deep.equal({action:'update', event:'disconnect', info:expectedCallInfo}); - }).timeout(timeout); - - it('should be able to parse query string', async ()=>{ - const now=''+Date.now(); - const ws=await createWebSocket(); - await ws.send(JSON.stringify({action:'registerListener'})); - await ws.receive1(); - - const c1=await createClient(); - const c2=await createClient(`now=${now}&before=123456789`); - expect(JSON.parse(await ws.receive1()).info.event.queryStringParameters).to.be.undefined; - expect(JSON.parse(await ws.receive1()).info.event.queryStringParameters).to.deep.equal({now, before:'123456789'}); - }).timeout(timeout); - - it('should be able to receive messages via REST API', async ()=>{ - const c1=await createClient(); - const c2=await createClient(); - const url=new URL(endpoint); - const signature = {service: 'execute-api', host:url.host, path:`${url.pathname}/@connections/${c2.id}`, method: 'POST', body:'Hello World!', headers:{'Content-Type':'text/plain'/*'application/text'*/}}; - aws4.sign(signature, {accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey}); - const res=await req.post(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers['Authorization']).set('Content-Type', signature.headers['Content-Type']).send('Hello World!'); - expect(res).to.have.status(200); - expect(await c2.ws.receive1()).to.equal('Hello World!'); - }).timeout(timeout); - - it('should receive error code when sending to non existing client via REST API', async ()=>{ - const c='aJz0Md6VoAMCIbQ='; - const url=new URL(endpoint); - const signature = {service: 'execute-api', host:url.host, path:`${url.pathname}/@connections/${c}`, method: 'POST', body:'Hello World!', headers:{'Content-Type':'text/plain'/*'application/text'*/}}; - aws4.sign(signature, {accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey}); - const res=await req.post(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers['Authorization']).set('Content-Type', signature.headers['Content-Type']).send('Hello World!'); - expect(res).to.have.status(410); - }).timeout(timeout); - - // UNABLE TO TEST HIS SCENARIO BECAUSE AWS DOESN'T RETURN ANYTHING - // it('should not receive anything when POSTing nothing', async ()=>{ - // const c1=await createClient(); - // const url=new URL(endpoint); - // const signature = {service: 'execute-api', host:url.host, path:`${url.pathname}/@connections/${c1.id}`, method: 'POST'/*, body:'Hello World!'*/, headers:{'Content-Type':'text/plain'/*'application/text'*/}}; - // aws4.sign(signature, {accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey}); - // const res=await req.post(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers['Authorization']).set('Content-Type', signature.headers['Content-Type']).send(/*'Hello World!'*/); - // expect(res).to.have.status(200); - // }).timeout(timeout); - - }); -}); \ No newline at end of file diff --git a/src/index.js b/src/index.js index d778345a3..b5907aa24 100755 --- a/src/index.js +++ b/src/index.js @@ -38,6 +38,7 @@ class Offline { this.exitCode = 0; this.clients = new Map(); this.wsActions = {}; + this.websocketsApiRouteSelectionExpression=serverless.service.provider.websocketsApiRouteSelectionExpression||'$request.body.action'; this.commands = { offline: { @@ -523,7 +524,17 @@ class Offline { const { initially, ws } = request.websocket(); if (!request.payload || initially) return h.response().code(204); const connection = this.clients.get(ws); - const action = request.payload.action || '$default'; + let actionName=null; + if (this.websocketsApiRouteSelectionExpression.startsWith('$request.body.')) { + actionName=request.payload; + if (typeof actionName === 'object') { + this.websocketsApiRouteSelectionExpression.replace('$request.body.', '').split('.').forEach((key)=>{ + if (actionName) actionName=actionName[key]; + }); + } else actionName=null; + } + if (typeof actionName != 'string') actionName=null; + const action = actionName || '$default'; debugLog(`action:${action} on connection=${connection.connectionId}`); const event = wsHelpers.createEvent(action, 'MESSAGE', connection, request.payload, this.options); const context = wsHelpers.createContext(action, this.options); From 3759b0619e7e6d93ff47292f5c2b2646a69ab110 Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Tue, 11 Jun 2019 23:36:24 +0300 Subject: [PATCH 26/71] Fix lint errors --- .../package-lock.json | 6 +++--- .../package.json | 2 +- .../manual_test_websocket_main/package-lock.json | 6 +++--- .../manual_test_websocket_main/package.json | 2 +- src/index.js | 15 ++++++++------- 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/package-lock.json b/manual_test_websocket/manual_test_websocket_RouteSelection/package-lock.json index 5316e3b25..ba31f9664 100644 --- a/manual_test_websocket/manual_test_websocket_RouteSelection/package-lock.json +++ b/manual_test_websocket/manual_test_websocket_RouteSelection/package-lock.json @@ -81,9 +81,9 @@ "dev": true }, "aws-sdk": { - "version": "2.472.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.472.0.tgz", - "integrity": "sha512-uFatrjfMSwC34VxdG9ollX6K61e+iaoE5ZHQ/OKeSoWx9HXs3AwJqIS90iBLEhaAm2WoTMFYAv0irMC8eMCu3g==", + "version": "2.473.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.473.0.tgz", + "integrity": "sha512-1Qr16lOcz4ANzl/oPQRR+fxchfvUx4PVQhUNnDU3FH9OBfU3Xj+Vh6bGYFbreFQgqIqXUTEuJR5pC44uK70YfA==", "requires": { "buffer": "4.9.1", "events": "1.1.1", diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/package.json b/manual_test_websocket/manual_test_websocket_RouteSelection/package.json index d580cdaa4..e2e5c1f63 100644 --- a/manual_test_websocket/manual_test_websocket_RouteSelection/package.json +++ b/manual_test_websocket/manual_test_websocket_RouteSelection/package.json @@ -13,7 +13,7 @@ "author": "", "license": "MIT", "dependencies": { - "aws-sdk": "^2.472.0" + "aws-sdk": "^2.473.0" }, "devDependencies": { "aws4": "^1.8.0", diff --git a/manual_test_websocket/manual_test_websocket_main/package-lock.json b/manual_test_websocket/manual_test_websocket_main/package-lock.json index 4c445744f..ba31f9664 100644 --- a/manual_test_websocket/manual_test_websocket_main/package-lock.json +++ b/manual_test_websocket/manual_test_websocket_main/package-lock.json @@ -81,9 +81,9 @@ "dev": true }, "aws-sdk": { - "version": "2.469.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.469.0.tgz", - "integrity": "sha512-VaIrO3aBX83gKkBPk9xM0RHmu7fmq76kaF0SqbsWlPImgxc5foJ4rBlRMMlmeNogFZZ/XTQdI+gkFDVosV14Ig==", + "version": "2.473.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.473.0.tgz", + "integrity": "sha512-1Qr16lOcz4ANzl/oPQRR+fxchfvUx4PVQhUNnDU3FH9OBfU3Xj+Vh6bGYFbreFQgqIqXUTEuJR5pC44uK70YfA==", "requires": { "buffer": "4.9.1", "events": "1.1.1", diff --git a/manual_test_websocket/manual_test_websocket_main/package.json b/manual_test_websocket/manual_test_websocket_main/package.json index 2d492794a..e2e5c1f63 100644 --- a/manual_test_websocket/manual_test_websocket_main/package.json +++ b/manual_test_websocket/manual_test_websocket_main/package.json @@ -13,7 +13,7 @@ "author": "", "license": "MIT", "dependencies": { - "aws-sdk": "^2.469.0" + "aws-sdk": "^2.473.0" }, "devDependencies": { "aws4": "^1.8.0", diff --git a/src/index.js b/src/index.js index b5907aa24..adf45e874 100755 --- a/src/index.js +++ b/src/index.js @@ -38,7 +38,7 @@ class Offline { this.exitCode = 0; this.clients = new Map(); this.wsActions = {}; - this.websocketsApiRouteSelectionExpression=serverless.service.provider.websocketsApiRouteSelectionExpression||'$request.body.action'; + this.websocketsApiRouteSelectionExpression = serverless.service.provider.websocketsApiRouteSelectionExpression || '$request.body.action'; this.commands = { offline: { @@ -524,16 +524,17 @@ class Offline { const { initially, ws } = request.websocket(); if (!request.payload || initially) return h.response().code(204); const connection = this.clients.get(ws); - let actionName=null; + let actionName = null; if (this.websocketsApiRouteSelectionExpression.startsWith('$request.body.')) { - actionName=request.payload; + actionName = request.payload; if (typeof actionName === 'object') { - this.websocketsApiRouteSelectionExpression.replace('$request.body.', '').split('.').forEach((key)=>{ - if (actionName) actionName=actionName[key]; + this.websocketsApiRouteSelectionExpression.replace('$request.body.', '').split('.').forEach(key => { + if (actionName) actionName = actionName[key]; }); - } else actionName=null; + } + else actionName = null; } - if (typeof actionName != 'string') actionName=null; + if (typeof actionName !== 'string') actionName = null; const action = actionName || '$default'; debugLog(`action:${action} on connection=${connection.connectionId}`); const event = wsHelpers.createEvent(action, 'MESSAGE', connection, request.payload, this.options); From 687f901514047d02585ac0c7e25efbd2e2edbe99 Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Wed, 12 Jun 2019 00:05:29 +0300 Subject: [PATCH 27/71] Merge leftovers --- package-lock.json | 144 +++++++++++++++++++++++++++++++++++++++++++--- package.json | 2 +- 2 files changed, 138 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index da73a5600..63cb9180a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "serverless-offline", - "version": "5.2.0", + "version": "5.3.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -785,6 +785,11 @@ "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", "dev": true }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1056,6 +1061,11 @@ "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==", "dev": true }, + "bignumber.js": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-8.1.1.tgz", + "integrity": "sha512-QD46ppGintwPGuL1KqmwhR0O+N2cZUg8JG/VzwI2e28sM9TqHjQB10lI4QAaMHVbLzwVLLAwEglpKPViWX+5NQ==" + }, "bl": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", @@ -1107,6 +1117,14 @@ } } }, + "boom": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-7.3.0.tgz", + "integrity": "sha512-Swpoyi2t5+GhOEGw8rEsKvTxFLIDiiKoUc2gsoV6Lyr43LHBIzch3k2MvYUs8RTROrIkVJ3Al0TkaOGjnb+B6A==", + "requires": { + "hoek": "6.x.x" + } + }, "boxen": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", @@ -1340,6 +1358,22 @@ "url-to-options": "^1.0.1" } }, + "cbor": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-4.1.4.tgz", + "integrity": "sha512-SqNWyQnnYtKAPLA7lupvuGKrEgoF2rR/7I9rXdmW/9uxtmKdltthHTf8hfLLN1SIkoAFwz/jb6+VZuaHv3Lv6Q==", + "requires": { + "bignumber.js": "^8.0.1", + "commander": "^2.19.0", + "json-text-sequence": "^0.1", + "nofilter": "^1.0.1" + } + }, + "cbor-js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cbor-js/-/cbor-js-0.1.0.tgz", + "integrity": "sha1-yAzmEg84fo+qdDcN/aIdlluPx/k=" + }, "chai": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", @@ -1556,8 +1590,7 @@ "commander": { "version": "2.20.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", - "dev": true + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" }, "component-emitter": { "version": "1.3.0", @@ -1920,6 +1953,11 @@ "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "dev": true }, + "delimit-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/delimit-stream/-/delimit-stream-0.1.0.tgz", + "integrity": "sha1-m4MZR3wOX4rrPONXrjBfwl6hzSs=" + }, "depcheck": { "version": "0.6.11", "resolved": "https://registry.npmjs.org/depcheck/-/depcheck-0.6.11.tgz", @@ -2222,6 +2260,17 @@ "iconv-lite": "~0.4.13" } }, + "encodr": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/encodr/-/encodr-1.2.0.tgz", + "integrity": "sha512-OHAfuXxoeXEeXFZ0Vu3CGegIVI1iuLLdVMy1EIVDBfvff1tMjVwRNBFuo5UbjBm3Efcu+GiIYGOt0H3NKDjPrw==", + "requires": { + "cbor": "4.1.4", + "cbor-js": "0.1.0", + "msgpack-lite": "0.1.26", + "utf8": "3.0.0" + } + }, "end-of-stream": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", @@ -2603,6 +2652,16 @@ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", "dev": true }, + "event-lite": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.2.tgz", + "integrity": "sha512-HnSYx1BsJ87/p6swwzv+2v6B4X+uxUteoDfRxsAb1S1BePzQqOLevVmkdA15GHJVd9A9Ok6wygUR18Hu0YeV9g==" + }, + "eventemitter3": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", + "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==" + }, "events": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", @@ -3157,6 +3216,18 @@ "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, + "hapi-plugin-websocket": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/hapi-plugin-websocket/-/hapi-plugin-websocket-2.0.16.tgz", + "integrity": "sha512-UE2JSrNIalXJmw4Qoi2FqhcguqTYQhjgPAVCtJUBhhZXD6IyA15guTdSeshBLxGfKcAFJmlpXs2LUMB25ARfOA==", + "requires": { + "boom": "7.3.0", + "hoek": "6.1.2", + "urijs": "1.19.1", + "websocket-framed": "1.2.0", + "ws": "6.1.2" + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -3279,6 +3350,11 @@ } } }, + "hoek": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.2.tgz", + "integrity": "sha512-6qhh/wahGYZHFSFw12tBbJw5fsAhhwrrG/y3Cs0YMTv2WzMnL0oLPnQJjv1QJvEfylRSOFuP+xCu+tdx0tD16Q==" + }, "homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -3340,8 +3416,7 @@ "ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "dev": true + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" }, "ignore": { "version": "4.0.6", @@ -3446,6 +3521,11 @@ } } }, + "int64-buffer": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/int64-buffer/-/int64-buffer-0.1.10.tgz", + "integrity": "sha1-J3siiofZWtd30HwTgyAiQGpHNCM=" + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -3706,8 +3786,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -3830,6 +3909,14 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", "dev": true }, + "json-text-sequence": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/json-text-sequence/-/json-text-sequence-0.1.1.tgz", + "integrity": "sha1-py8hfcSvxGKf/1/rME3BvVGi89I=", + "requires": { + "delimit-stream": "0.1.0" + } + }, "jsonata": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/jsonata/-/jsonata-1.6.4.tgz", @@ -4520,6 +4607,17 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" }, + "msgpack-lite": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/msgpack-lite/-/msgpack-lite-0.1.26.tgz", + "integrity": "sha1-3TxQsm8FnyXn7e42REGDWOKprYk=", + "requires": { + "event-lite": "^0.1.1", + "ieee754": "^1.1.8", + "int64-buffer": "^0.1.9", + "isarray": "^1.0.0" + } + }, "mute-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", @@ -4620,6 +4718,11 @@ "is-stream": "^1.0.1" } }, + "nofilter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-1.0.2.tgz", + "integrity": "sha512-d38SORxm9UNoDsnPXajV9nBEebKX4/paXAlyRGnSjZuFbLLZDFUO4objr+tbybqsbqGXDWllb6gQoKUDc9q3Cg==" + }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -7157,6 +7260,11 @@ "punycode": "^2.1.0" } }, + "urijs": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.1.tgz", + "integrity": "sha512-xVrGVi94ueCJNrBSTjWqjvtgvl3cyOTThp2zaMaFNGp3F542TR6sM3f2o8RqZl+AwteClSVmoCyt0ka4RjQOQg==" + }, "urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", @@ -7202,6 +7310,11 @@ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "dev": true }, + "utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", + "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==" + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -7247,6 +7360,15 @@ "integrity": "sha1-oW0CXrkxvQO1LzCMrtD0D86+lTI=", "dev": true }, + "websocket-framed": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/websocket-framed/-/websocket-framed-1.2.0.tgz", + "integrity": "sha512-rnVf9NisrPAKIzB0LLgLdnbiElRNZSeahKKXcicxzOxJdW4ZaCE7xR7nheBIoN2j++2fk6FD1GTg9d+rw/X9+g==", + "requires": { + "encodr": "1.2.0", + "eventemitter3": "3.1.0" + } + }, "whatwg-fetch": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz", @@ -7375,6 +7497,14 @@ "signal-exit": "^3.0.2" } }, + "ws": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.2.tgz", + "integrity": "sha512-rfUqzvz0WxmSXtJpPMX2EeASXabOrSMk1ruMOV3JBTBjo4ac2lDjGGsbQSyxj8Odhw5fBib8ZKEjDNvgouNKYw==", + "requires": { + "async-limiter": "~1.0.0" + } + }, "xdg-basedir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", diff --git a/package.json b/package.json index 07cf458ee..6d8528536 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,7 @@ "Selcuk Cihan (https://github.com/selcukcihan)", "G Roques (https://github.com/gbroques)", "Dustin Belliston (https://github.com/dwbelliston)", - "kobanyan (https://github.com/kobanyan)" + "kobanyan (https://github.com/kobanyan)", "Ram Hardy (https://github.com/computerpunc)" ], "dependencies": { From b9b0eefe72df96ead74bbd1b4f8f016cab91cc03 Mon Sep 17 00:00:00 2001 From: computerpunc Date: Wed, 12 Jun 2019 00:11:54 +0300 Subject: [PATCH 28/71] Update README --- manual_test_websocket/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/manual_test_websocket/README.md b/manual_test_websocket/README.md index b907d45ff..7eb75bafd 100644 --- a/manual_test_websocket/README.md +++ b/manual_test_websocket/README.md @@ -8,6 +8,7 @@ Set AWS credentials, e.g.: `export AWS_PROFILE=...` To start AWS DynamoDB locally (can run only after first deploying locally): `sls dynamodb install` `sls dynamodb start` +### In either main or RouteSelection folder the following: ## Deploying locally From b6c1095319fe1802a93334234875bbe7e06c7f60 Mon Sep 17 00:00:00 2001 From: computerpunc Date: Wed, 12 Jun 2019 00:48:18 +0300 Subject: [PATCH 29/71] Update README about WebSocket --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index aeb5b9d2d..194afbfe4 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ This plugin is updated by its users, I just do maintenance and ensure that PRs a * [Custom headers](#custom-headers) * [Environment variables](#environment-variables) * [AWS API Gateway features](#aws-api-gateway-features) +* [WebSocket](#websocket) * [Usage with Webpack](#usage-with-webpack) * [Velocity nuances](#velocity-nuances) * [Debug process](#debug-process) @@ -358,6 +359,31 @@ resources: To disable the model validation you can use `--disableModelValidation`. +## WebSocket +`This has experimental functionality. Please report any bugs or missing features.` + +serverless-offline suports running a WebSocket local endpoint. The `port` used is one port above the HTTP port. +By default, HTTP port is 3000 and hence WebSocket port is 3001. + +Usage in order to send messages back to clients: + +`POST http://localhost:{port+1}/@connections/{connectionId}` + +Or, + +``` +let endpoint=event.apiGatewayUrl; +if (!endpoint) endpoint = event.requestContext.domainName+'/'+event.requestContext.stage; +const apiVersion='2018-11-29'; +const apiGM=new API.ApiGatewayManagementApi({ apiVersion, endpoint }); +apiGM.postToConnection({ConnectionId, Data}); +``` +Where the `event` is received in the lambda hanlder function. + +There's support for `websocketsApiRouteSelectionExpression` in it's basic form: `$request.body.x.y.z`, where the default value is `$request.body.action`. + +Authorizers and WSS:// are currectly not supoprted in serverless-offline. + ## Usage with Webpack Use [serverless-webpack](https://github.com/serverless-heaven/serverless-webpack) to compile and bundle your ES-next code From e855a4a93ead5513a1bbcba7181fe5d37318e39f Mon Sep 17 00:00:00 2001 From: Daniel Nalborczyk Date: Tue, 11 Jun 2019 20:40:50 -0400 Subject: [PATCH 30/71] Export on exports --- src/websocketHelpers.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/websocketHelpers.js b/src/websocketHelpers.js index bb4303e5b..5a11e6994 100644 --- a/src/websocketHelpers.js +++ b/src/websocketHelpers.js @@ -31,23 +31,23 @@ const createRequestContext = (action, eventType, connection) => { connectionId:connection.connectionId, apiId: 'private', }; - + return requestContext; }; -module.exports.createEvent = (action, eventType, connection, payload, options) => { - const event = { +exports.createEvent = (action, eventType, connection, payload, options) => { + const event = { requestContext: createRequestContext(action, eventType, connection), body: JSON.stringify(payload), isBase64Encoded: false, apiGatewayUrl: `http${options.httpsProtocol ? 's' : ''}://${options.host}:${options.port + 1}`, }; - + return event; }; -module.exports.createConnectEvent = (action, eventType, connection, options) => { - const headers = { +exports.createConnectEvent = (action, eventType, connection, options) => { + const headers = { Host: 'localhost', 'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits', 'Sec-WebSocket-Key': `${randomId()}`, @@ -66,12 +66,12 @@ module.exports.createConnectEvent = (action, eventType, connection, options) => apiGatewayUrl: `http${options.httpsProtocol ? 's' : ''}://${options.host}:${options.port + 1}`, isBase64Encoded: false, }; - + return event; }; -module.exports.createDisconnectEvent = (action, eventType, connection, options) => { - const headers = { +exports.createDisconnectEvent = (action, eventType, connection, options) => { + const headers = { Host: 'localhost', 'x-api-key': '', 'x-restapi': '', @@ -85,11 +85,11 @@ module.exports.createDisconnectEvent = (action, eventType, connection, options) apiGatewayUrl: `http${options.httpsProtocol ? 's' : ''}://${options.host}:${options.port + 1}`, isBase64Encoded: false, }; - + return event; }; -module.exports.createContext = action => { +exports.createContext = action => { const context = { awsRequestId: `offline_awsRequestId_for_${action}`, callbackWaitsForEmptyEventLoop: true, From 53da9c1e0364ff5320c01a724601550b5eee5e60 Mon Sep 17 00:00:00 2001 From: Daniel Nalborczyk Date: Tue, 11 Jun 2019 20:41:53 -0400 Subject: [PATCH 31/71] Extract multi-value-headers function --- src/websocketHelpers.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/websocketHelpers.js b/src/websocketHelpers.js index 5a11e6994..9092cd138 100644 --- a/src/websocketHelpers.js +++ b/src/websocketHelpers.js @@ -1,5 +1,15 @@ const { randomId } = require('./utils'); +// TODO this should be probably moved to utils, and combined with other header +// functions and utilities +function createMultiValueHeaders(headers) { + return Object.entries(headers).reduce((acc, [key, value]) => { + acc[key] = [value]; + + return acc; + }, {}); +} + const createRequestContext = (action, eventType, connection) => { const now = new Date(); const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; @@ -57,9 +67,8 @@ exports.createConnectEvent = (action, eventType, connection, options) => { 'X-Forwarded-Port': `${options.port + 1}`, 'X-Forwarded-Proto': `http${options.httpsProtocol ? 's' : ''}`, }; - const multiValueHeaders = { ...headers }; - Object.keys(multiValueHeaders).map(key => multiValueHeaders[key] = [multiValueHeaders[key]]); - const event = { + const multiValueHeaders = createMultiValueHeaders(headers); + const event = { headers, multiValueHeaders, requestContext: createRequestContext(action, eventType, connection), @@ -76,9 +85,8 @@ exports.createDisconnectEvent = (action, eventType, connection, options) => { 'x-api-key': '', 'x-restapi': '', }; - const multiValueHeaders = { ...headers }; - Object.keys(multiValueHeaders).map(key => multiValueHeaders[key] = [multiValueHeaders[key]]); - const event = { + const multiValueHeaders = createMultiValueHeaders(headers); + const event = { headers, multiValueHeaders, requestContext: createRequestContext(action, eventType, connection), From d802dcf95145ec236ebdf030c50e977da6bfe3b0 Mon Sep 17 00:00:00 2001 From: Daniel Nalborczyk Date: Tue, 11 Jun 2019 21:30:55 -0400 Subject: [PATCH 32/71] Generate collision resistent ids with cuid (project-wide) --- package-lock.json | 5 +++++ package.json | 1 + src/createLambdaContext.js | 4 ++-- src/createLambdaProxyContext.js | 4 ++-- src/createVelocityContext.js | 4 ++-- src/functionHelper.js | 4 ++-- src/index.js | 6 +++--- src/utils.js | 7 +++++-- src/websocketHelpers.js | 18 +++++++++--------- 9 files changed, 31 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index 63cb9180a..ed9d06df0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1776,6 +1776,11 @@ "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", "dev": true }, + "cuid": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/cuid/-/cuid-2.1.6.tgz", + "integrity": "sha512-ZFp7PS6cSYMJNch9fc3tyHdE4T8TDo3Y5qAxb0KSA9mpiYDo7z9ql1CznFuuzxea9STVIDy0tJWm2lYiX2ZU1Q==" + }, "currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", diff --git a/package.json b/package.json index 6d8528536..03448f54f 100644 --- a/package.json +++ b/package.json @@ -133,6 +133,7 @@ "@hapi/cryptiles": "^4.2.0", "@hapi/h2o2": "^8.3.0", "@hapi/hapi": "^18.3.1", + "cuid": "^2.1.6", "hapi-plugin-websocket": "^2.0.16", "js-string-escape": "^1.0.1", "jsonpath-plus": "^0.20.0", diff --git a/src/createLambdaContext.js b/src/createLambdaContext.js index 985118d12..eebd95616 100644 --- a/src/createLambdaContext.js +++ b/src/createLambdaContext.js @@ -1,6 +1,6 @@ 'use strict'; -const { randomId } = require('./utils'); +const { getUniqueId } = require('./utils'); // https://docs.aws.amazon.com/lambda/latest/dg/limits.html // default function timeout in seconds @@ -26,7 +26,7 @@ module.exports = function createLambdaContext(fun, provider, cb) { getRemainingTimeInMillis: () => endTime - new Date().getTime(), // properties - awsRequestId: `offline_awsRequestId_${randomId()}`, + awsRequestId: `offline_awsRequestId_${getUniqueId()}`, clientContext: {}, functionName, functionVersion: `offline_functionVersion_for_${functionName}`, diff --git a/src/createLambdaProxyContext.js b/src/createLambdaProxyContext.js index d52c89a50..643785e25 100644 --- a/src/createLambdaProxyContext.js +++ b/src/createLambdaProxyContext.js @@ -5,7 +5,7 @@ const { normalizeMultiValueQuery, normalizeQuery, nullIfEmpty, - randomId, + getUniqueId, } = require('./utils'); /* @@ -79,7 +79,7 @@ module.exports = function createLambdaProxyContext(request, options, stageVariab resourceId: 'offlineContext_resourceId', apiId: 'offlineContext_apiId', stage: options.stage, - requestId: `offlineContext_requestId_${randomId()}`, + requestId: `offlineContext_requestId_${getUniqueId()}`, identity: { cognitoIdentityPoolId: process.env.SLS_COGNITO_IDENTITY_POOL_ID || 'offlineContext_cognitoIdentityPoolId', accountId: process.env.SLS_ACCOUNT_ID || 'offlineContext_accountId', diff --git a/src/createVelocityContext.js b/src/createVelocityContext.js index cd39caf5e..013f2b511 100644 --- a/src/createVelocityContext.js +++ b/src/createVelocityContext.js @@ -2,7 +2,7 @@ const jsEscapeString = require('js-string-escape'); const { decode } = require('jsonwebtoken'); -const { isPlainObject, randomId } = require('./utils'); +const { isPlainObject, getUniqueId } = require('./utils'); const jsonPath = require('./jsonPath'); function escapeJavaScript(x) { @@ -66,7 +66,7 @@ module.exports = function createVelocityContext(request, options, payload) { userAgent: request.headers['user-agent'] || '', userArn: 'offlineContext_userArn', }, - requestId: `offlineContext_requestId_${randomId()}`, + requestId: `offlineContext_requestId_${getUniqueId()}`, resourceId: 'offlineContext_resourceId', resourcePath: request.route.path, stage: options.stage, diff --git a/src/functionHelper.js b/src/functionHelper.js index 60e0634dc..291a1ebb1 100644 --- a/src/functionHelper.js +++ b/src/functionHelper.js @@ -4,7 +4,7 @@ const { fork, spawn } = require('child_process'); const path = require('path'); const trimNewlines = require('trim-newlines'); const debugLog = require('./debugLog'); -const { randomId } = require('./utils'); +const { getUniqueId } = require('./utils'); const handlerCache = {}; const messageCallbacks = {}; @@ -150,7 +150,7 @@ module.exports = { } return (event, context, done) => { - const id = randomId(); + const id = getUniqueId(); messageCallbacks[id] = done; handlerContext.inflight.add(id); handlerContext.process.send(Object.assign({}, funOptions, { id, event, context })); diff --git a/src/index.js b/src/index.js index fa8b65f28..43cb0a98a 100755 --- a/src/index.js +++ b/src/index.js @@ -21,7 +21,7 @@ const createAuthScheme = require('./createAuthScheme'); const functionHelper = require('./functionHelper'); const Endpoint = require('./Endpoint'); const parseResources = require('./parseResources'); -const { createDefaultApiKey, detectEncoding, randomId } = require('./utils'); +const { createDefaultApiKey, detectEncoding, getUniqueId } = require('./utils'); const authFunctionNameExtractor = require('./authFunctionNameExtractor'); const requestBodyValidator = require('./requestBodyValidator'); const wsHelpers = require('./websocketHelpers'); @@ -500,7 +500,7 @@ class Offline { }; const queryStringParameters = parseQuery(req.url); - const connection = { connectionId:randomId(), connectionTime:Date.now() }; + const connection = { connectionId:getUniqueId(), connectionTime:Date.now() }; debugLog(`connect:${connection.connectionId}`); this.clients.set(ws, connection); @@ -843,7 +843,7 @@ class Offline { } } // Shared mutable state is the root of all evil they say - const requestId = randomId(); + const requestId = getUniqueId(); this.requests[requestId] = { done: false }; this.currentRequestId = requestId; diff --git a/src/utils.js b/src/utils.js index 85770f04f..af46d883c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,12 +1,11 @@ 'use strict'; +const cuid = require('cuid'); const { createHash } = require('crypto'); module.exports = { toPlainOrEmptyObject: obj => typeof obj === 'object' && !Array.isArray(obj) ? obj : {}, - randomId: () => Math.random().toString(10).slice(2), - nullIfEmpty: o => o && (Object.keys(o).length > 0 ? o : null), isPlainObject: obj => typeof obj === 'object' && !Array.isArray(obj), @@ -43,4 +42,8 @@ module.exports = { createDefaultApiKey() { return createHash('md5').digest('hex'); }, + + getUniqueId() { + return cuid(); + }, }; diff --git a/src/websocketHelpers.js b/src/websocketHelpers.js index 9092cd138..3926be85e 100644 --- a/src/websocketHelpers.js +++ b/src/websocketHelpers.js @@ -1,4 +1,4 @@ -const { randomId } = require('./utils'); +const { getUniqueId } = require('./utils'); // TODO this should be probably moved to utils, and combined with other header // functions and utilities @@ -13,11 +13,11 @@ function createMultiValueHeaders(headers) { const createRequestContext = (action, eventType, connection) => { const now = new Date(); const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; - const requestContext = { + const requestContext = { routeKey: action, - messageId: `${randomId()}`, + messageId: `${getUniqueId()}`, eventType, - extendedRequestId: `${randomId()}`, + extendedRequestId: `${getUniqueId()}`, requestTime: `${now.getUTCDate()}/${months[now.getUTCMonth()]}/${now.getUTCFullYear()}:${now.getUTCHours()}:${now.getUTCMinutes()}:${now.getSeconds()} +0000`, messageDirection: 'IN', stage: 'local', @@ -36,10 +36,10 @@ const createRequestContext = (action, eventType, connection) => { accessKey: null, cognitoAuthenticationProvider: null, user: null }, - requestId: `${randomId()}`, + requestId: `${getUniqueId()}`, domainName: 'localhost', connectionId:connection.connectionId, - apiId: 'private', + apiId: 'private', }; return requestContext; @@ -60,12 +60,12 @@ exports.createConnectEvent = (action, eventType, connection, options) => { const headers = { Host: 'localhost', 'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits', - 'Sec-WebSocket-Key': `${randomId()}`, + 'Sec-WebSocket-Key': `${getUniqueId()}`, 'Sec-WebSocket-Version': '13', - 'X-Amzn-Trace-Id': `Root=${randomId()}`, + 'X-Amzn-Trace-Id': `Root=${getUniqueId()}`, 'X-Forwarded-For': '127.0.0.1', 'X-Forwarded-Port': `${options.port + 1}`, - 'X-Forwarded-Proto': `http${options.httpsProtocol ? 's' : ''}`, + 'X-Forwarded-Proto': `http${options.httpsProtocol ? 's' : ''}`, }; const multiValueHeaders = createMultiValueHeaders(headers); const event = { From aa4f9770d7c17eb554f8601632261e2b35d7b80a Mon Sep 17 00:00:00 2001 From: Daniel Nalborczyk Date: Thu, 13 Jun 2019 19:53:33 -0400 Subject: [PATCH 33/71] Add formatToClfTime function --- package-lock.json | 5 +++++ package.json | 1 + src/websocketHelpers.js | 19 +++++++++++++++++-- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index ed9d06df0..e1ad4015c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4244,6 +4244,11 @@ "integrity": "sha1-mgD3bco26yP6BTUK/htYXUKZ5ks=", "dev": true }, + "luxon": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.16.0.tgz", + "integrity": "sha512-qaqB+JwpGwtl7UbIXng3A/l4W/ySBr8drQvwtMLZBMiLD2V+0fEnPWMrs+UjnIy9PsktazQaKvwDUCLzoWz0Hw==" + }, "make-dir": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", diff --git a/package.json b/package.json index 03448f54f..37828028d 100644 --- a/package.json +++ b/package.json @@ -139,6 +139,7 @@ "jsonpath-plus": "^0.20.0", "jsonschema": "^1.2.4", "jsonwebtoken": "^8.5.1", + "luxon": "^1.16.0", "trim-newlines": "^3.0.0", "velocityjs": "^1.1.3" }, diff --git a/src/websocketHelpers.js b/src/websocketHelpers.js index 3926be85e..1a695ef62 100644 --- a/src/websocketHelpers.js +++ b/src/websocketHelpers.js @@ -1,3 +1,4 @@ +const { DateTime } = require('luxon'); const { getUniqueId } = require('./utils'); // TODO this should be probably moved to utils, and combined with other header @@ -10,15 +11,29 @@ function createMultiValueHeaders(headers) { }, {}); } +// CLF -> Common Log Format +// https://httpd.apache.org/docs/1.3/logs.html#common +// [day/month/year:hour:minute:second zone] +// day = 2*digit +// month = 3*letter +// year = 4*digit +// hour = 2*digit +// minute = 2*digit +// second = 2*digit +// zone = (`+' | `-') 4*digit +function formatToClfTime(date) { + return DateTime.fromJSDate(date).toFormat('dd/MMM/yyyy:HH:mm:ss ZZZ'); +} + const createRequestContext = (action, eventType, connection) => { const now = new Date(); - const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + const requestContext = { routeKey: action, messageId: `${getUniqueId()}`, eventType, extendedRequestId: `${getUniqueId()}`, - requestTime: `${now.getUTCDate()}/${months[now.getUTCMonth()]}/${now.getUTCFullYear()}:${now.getUTCHours()}:${now.getUTCMinutes()}:${now.getSeconds()} +0000`, + requestTime: formatToClfTime(now), messageDirection: 'IN', stage: 'local', connectedAt: connection.connectionTime, From 2df8ef8427271d50a1aaf68631a05d3a09363518 Mon Sep 17 00:00:00 2001 From: Daniel Nalborczyk Date: Thu, 13 Jun 2019 20:15:15 -0400 Subject: [PATCH 34/71] Order props --- src/websocketHelpers.js | 57 +++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/src/websocketHelpers.js b/src/websocketHelpers.js index 1a695ef62..091212733 100644 --- a/src/websocketHelpers.js +++ b/src/websocketHelpers.js @@ -29,32 +29,33 @@ const createRequestContext = (action, eventType, connection) => { const now = new Date(); const requestContext = { - routeKey: action, - messageId: `${getUniqueId()}`, + apiId: 'private', + connectedAt: connection.connectionTime, + connectionId:connection.connectionId, + domainName: 'localhost', eventType, extendedRequestId: `${getUniqueId()}`, - requestTime: formatToClfTime(now), + identity: { + accountId: null, + accessKey: null, + caller: null, + cognitoAuthenticationProvider: null, + cognitoAuthenticationType: null, + cognitoIdentityId: null, + cognitoIdentityPoolId: null, + principalOrgId: null, + sourceIp: '127.0.0.1', + user: null, + userAgent: null, + userArn: null, + }, messageDirection: 'IN', - stage: 'local', - connectedAt: connection.connectionTime, - requestTimeEpoch: now.getTime(), - identity: - { cognitoIdentityPoolId: null, - cognitoIdentityId: null, - principalOrgId: null, - cognitoAuthenticationType: null, - userArn: null, - userAgent: null, - accountId: null, - caller: null, - sourceIp: '127.0.0.1', - accessKey: null, - cognitoAuthenticationProvider: null, - user: null }, + messageId: `${getUniqueId()}`, requestId: `${getUniqueId()}`, - domainName: 'localhost', - connectionId:connection.connectionId, - apiId: 'private', + requestTime: formatToClfTime(now), + requestTimeEpoch: now.getTime(), + routeKey: action, + stage: 'local', }; return requestContext; @@ -62,10 +63,10 @@ const createRequestContext = (action, eventType, connection) => { exports.createEvent = (action, eventType, connection, payload, options) => { const event = { - requestContext: createRequestContext(action, eventType, connection), + apiGatewayUrl: `http${options.httpsProtocol ? 's' : ''}://${options.host}:${options.port + 1}`, body: JSON.stringify(payload), isBase64Encoded: false, - apiGatewayUrl: `http${options.httpsProtocol ? 's' : ''}://${options.host}:${options.port + 1}`, + requestContext: createRequestContext(action, eventType, connection), }; return event; @@ -84,11 +85,11 @@ exports.createConnectEvent = (action, eventType, connection, options) => { }; const multiValueHeaders = createMultiValueHeaders(headers); const event = { + apiGatewayUrl: `http${options.httpsProtocol ? 's' : ''}://${options.host}:${options.port + 1}`, headers, + isBase64Encoded: false, multiValueHeaders, requestContext: createRequestContext(action, eventType, connection), - apiGatewayUrl: `http${options.httpsProtocol ? 's' : ''}://${options.host}:${options.port + 1}`, - isBase64Encoded: false, }; return event; @@ -102,11 +103,11 @@ exports.createDisconnectEvent = (action, eventType, connection, options) => { }; const multiValueHeaders = createMultiValueHeaders(headers); const event = { + apiGatewayUrl: `http${options.httpsProtocol ? 's' : ''}://${options.host}:${options.port + 1}`, headers, + isBase64Encoded: false, multiValueHeaders, requestContext: createRequestContext(action, eventType, connection), - apiGatewayUrl: `http${options.httpsProtocol ? 's' : ''}://${options.host}:${options.port + 1}`, - isBase64Encoded: false, }; return event; From fd0637da62b696ff1997435b6a59f2e6c0bd9cbe Mon Sep 17 00:00:00 2001 From: Daniel Nalborczyk Date: Thu, 13 Jun 2019 20:16:47 -0400 Subject: [PATCH 35/71] Remove unused options --- src/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 43cb0a98a..7e6c92462 100755 --- a/src/index.js +++ b/src/index.js @@ -515,7 +515,7 @@ class Offline { debugLog(`disconnect:${connection.connectionId}`); this.clients.delete(ws); const event = wsHelpers.createDisconnectEvent('$disconnect', 'DISCONNECT', connection, this.options); - const context = wsHelpers.createContext('$disconnect', this.options); + const context = wsHelpers.createContext('$disconnect'); doAction(ws, connection.connectionId, '$disconnect', event, context); }, @@ -540,7 +540,7 @@ class Offline { const action = actionName || '$default'; debugLog(`action:${action} on connection=${connection.connectionId}`); const event = wsHelpers.createEvent(action, 'MESSAGE', connection, request.payload, this.options); - const context = wsHelpers.createContext(action, this.options); + const context = wsHelpers.createContext(action); doAction(ws, connection.connectionId, action, event, context, true); From 8a699abdf684b9f6d71de969cbd1ae4ff3e5ac88 Mon Sep 17 00:00:00 2001 From: Daniel Nalborczyk Date: Fri, 14 Jun 2019 09:37:56 -0400 Subject: [PATCH 36/71] Add websocket port option --- README.md | 6 +++--- src/index.js | 36 +++++++++++++++++++++--------------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 194afbfe4..764228cb1 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ All CLI options are optional: --location -l The root location of the handlers' files. Defaults to the current directory --host -o Host name to listen on. Default: localhost --port -P Port to listen on. Default: 3000 +--wsPort WebSocket port to listen on. Default: 3001 --stage -s The stage used to populate your templates. Default: the first stage found in your project. --region -r The region used to populate your templates. Default: the first region for the first stage found. --noTimeout -t Disables the timeout feature. @@ -362,11 +363,10 @@ To disable the model validation you can use `--disableModelValidation`. ## WebSocket `This has experimental functionality. Please report any bugs or missing features.` -serverless-offline suports running a WebSocket local endpoint. The `port` used is one port above the HTTP port. -By default, HTTP port is 3000 and hence WebSocket port is 3001. +serverless-offline suports running a WebSocket local endpoint. Usage in order to send messages back to clients: - + `POST http://localhost:{port+1}/@connections/{connectionId}` Or, diff --git a/src/index.js b/src/index.js index 7e6c92462..9cc349679 100755 --- a/src/index.js +++ b/src/index.js @@ -147,6 +147,9 @@ class Offline { useSeparateProcesses: { usage: 'Uses separate node processes for handlers', }, + wsPort: { + usage: 'Websocket port to listen on. Default: 3001', + }, }, }, }; @@ -288,6 +291,7 @@ class Offline { resourceRoutes: false, skipCacheInvalidation: false, useSeparateProcesses: false, + wsPort: 3001, }; // In the constructor, stage and regions are set to undefined @@ -391,7 +395,7 @@ class Offline { // start COPY PASTE FROM HTTP SERVER CODE const serverOptions = { host: this.options.host, - port: this.options.port + 1, + port: this.options.wsPort, router: { stripTrailingSlash: !this.options.preserveTrailingSlash, // removes trailing slashes on incoming paths. }, @@ -453,32 +457,32 @@ class Offline { const doAction = (ws, connectionId, name, event, context, doDeafultAction/* , onError */) => { const sendError = err => { - if (ws.readyState === /* OPEN */1) ws.send(JSON.stringify({ message:'Internal server error', connectionId, requestId:'1234567890' })); + if (ws.readyState === /* OPEN */1) ws.send(JSON.stringify({ message:'Internal server error', connectionId, requestId:'1234567890' })); debugLog(`Error in handler of action ${action}`, err); }; let action = this.wsActions[name]; if (!action && doDeafultAction) action = this.wsActions.$default; if (!action) return; let p = null; - try { + try { p = action.handler(event, context, err => { if (!err) return; sendError(err); }); - } + } catch (err) { sendError(err); } - if (p) { - p.catch(err => { + if (p) { + p.catch(err => { sendError(err); }); } }; - + this.wsServer.route({ - method: 'POST', + method: 'POST', path: '/', config: { payload: { output: 'data', parse: true, allow: 'application/json' }, @@ -522,6 +526,7 @@ class Offline { }, }, }, + handler: (request, h) => { const { initially, ws } = request.websocket(); if (!request.payload || initially) return h.response().code(204); @@ -533,7 +538,7 @@ class Offline { this.websocketsApiRouteSelectionExpression.replace('$request.body.', '').split('.').forEach(key => { if (actionName) actionName = actionName[key]; }); - } + } else actionName = null; } if (typeof actionName !== 'string') actionName = null; @@ -547,11 +552,13 @@ class Offline { return h.response().code(204); }, }); + this.wsServer.route({ method: 'GET', path: '/{path*}', handler: (request, h) => h.response().code(426), }); + this.wsServer.route({ method: 'POST', path: '/@connections/{connectionId}', @@ -565,7 +572,7 @@ class Offline { return undefined; }; - + const ws = getByConnectionId(this.clients, request.params.connectionId); if (!ws) return h.response().code(410); if (!request.payload) return ''; @@ -698,7 +705,7 @@ class Offline { fun.events.forEach(event => { if (event.websocket) { this._createWsAction(fun, funName, servicePath, funOptions, event); - + return; } if (!event.http) return; @@ -1373,16 +1380,15 @@ class Offline { await this.wsServer.start(); } catch (e) { - console.error(`Unexpected error while starting serverless-offline server on port ${this.options.port + 1}:`, e); + console.error(`Unexpected error while starting serverless-offline websocket server on port ${this.options.wsPort}:`, e); process.exit(1); } this.printBlankLine(); - this.serverlessLog(`Offline listening on ws${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port + 1}`); + this.serverlessLog(`Offline listening on ws${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.wsPort}`); this.printBlankLine(); - this.serverlessLog(`Offline listening on http${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port + 1}/@connections/{connectionId}`); - + this.serverlessLog(`Offline listening on http${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.wsPort}/@connections/{connectionId}`); } end() { From 7c341783f6a3109b4013502cc7445fba7d7556dc Mon Sep 17 00:00:00 2001 From: Daniel Nalborczyk Date: Fri, 14 Jun 2019 09:38:24 -0400 Subject: [PATCH 37/71] Fix spelling --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 764228cb1..9d35e6c89 100644 --- a/README.md +++ b/README.md @@ -378,11 +378,11 @@ const apiVersion='2018-11-29'; const apiGM=new API.ApiGatewayManagementApi({ apiVersion, endpoint }); apiGM.postToConnection({ConnectionId, Data}); ``` -Where the `event` is received in the lambda hanlder function. +Where the `event` is received in the lambda handler function. There's support for `websocketsApiRouteSelectionExpression` in it's basic form: `$request.body.x.y.z`, where the default value is `$request.body.action`. -Authorizers and WSS:// are currectly not supoprted in serverless-offline. +Authorizers and wss:// are currectly not supported in serverless-offline. ## Usage with Webpack From 193e28292e724e9278ebe0cdb02b4dfe50490638 Mon Sep 17 00:00:00 2001 From: Daniel Nalborczyk Date: Sat, 15 Jun 2019 14:53:04 -0400 Subject: [PATCH 38/71] Separate ApiGateway and ApiGatewayWebSockets from Plugin --- src/ApiGateway.js | 1015 ++++++++++++++++++++++++++++ src/ApiGatewayWebSocket.js | 344 ++++++++++ src/index.js | 1284 +----------------------------------- 3 files changed, 1378 insertions(+), 1265 deletions(-) create mode 100644 src/ApiGateway.js create mode 100644 src/ApiGatewayWebSocket.js diff --git a/src/ApiGateway.js b/src/ApiGateway.js new file mode 100644 index 000000000..7587c936c --- /dev/null +++ b/src/ApiGateway.js @@ -0,0 +1,1015 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const { performance, PerformanceObserver } = require('perf_hooks'); +const hapi = require('@hapi/hapi'); +const h2o2 = require('@hapi/h2o2'); +const debugLog = require('./debugLog'); +const jsonPath = require('./jsonPath'); +const createLambdaContext = require('./createLambdaContext'); +const createVelocityContext = require('./createVelocityContext'); +const createLambdaProxyContext = require('./createLambdaProxyContext'); +const renderVelocityTemplateObject = require('./renderVelocityTemplateObject'); +const createAuthScheme = require('./createAuthScheme'); +const functionHelper = require('./functionHelper'); +const Endpoint = require('./Endpoint'); +const parseResources = require('./parseResources'); +const { detectEncoding, getUniqueId } = require('./utils'); +const authFunctionNameExtractor = require('./authFunctionNameExtractor'); +const requestBodyValidator = require('./requestBodyValidator'); + +module.exports = class ApiGateway { + constructor(serverless, options, velocityContextOptions) { + this.serverless = serverless; + this.service = serverless.service; + this.serverlessLog = serverless.cli.log.bind(serverless.cli); + this.options = options; + this.exitCode = 0; + this.clients = new Map(); + + this.requests = {}; + this.velocityContextOptions = velocityContextOptions; + } + + printBlankLine() { + console.log(); + } + + logPluginIssue() { + this.serverlessLog('If you think this is an issue with the plugin please submit it, thanks!'); + this.serverlessLog('https://github.com/dherault/serverless-offline/issues'); + } + + _createServer() { + const serverOptions = { + host: this.options.host, + port: this.options.port, + router: { + stripTrailingSlash: !this.options.preserveTrailingSlash, // removes trailing slashes on incoming paths. + }, + }; + + const httpsDir = this.options.httpsProtocol; + + // HTTPS support + if (typeof httpsDir === 'string' && httpsDir.length > 0) { + serverOptions.tls = { + key: fs.readFileSync(path.resolve(httpsDir, 'key.pem'), 'ascii'), + cert: fs.readFileSync(path.resolve(httpsDir, 'cert.pem'), 'ascii'), + }; + } + + serverOptions.state = this.options.enforceSecureCookies ? { + isHttpOnly: true, + isSecure: true, + isSameSite: false, + } : { + isHttpOnly: false, + isSecure: false, + isSameSite: false, + }; + + // Hapijs server creation + this.server = hapi.server(serverOptions); + + this.server.register(h2o2).catch(err => err && this.serverlessLog(err)); + + // Enable CORS preflight response + this.server.ext('onPreResponse', (request, h) => { + if (request.headers.origin) { + const response = request.response.isBoom ? request.response.output : request.response; + + response.headers['access-control-allow-origin'] = request.headers.origin; + response.headers['access-control-allow-credentials'] = 'true'; + + if (request.method === 'options') { + response.statusCode = 200; + response.headers['access-control-expose-headers'] = 'content-type, content-length, etag'; + response.headers['access-control-max-age'] = 60 * 10; + + if (request.headers['access-control-request-headers']) { + response.headers['access-control-allow-headers'] = request.headers['access-control-request-headers']; + } + + if (request.headers['access-control-request-method']) { + response.headers['access-control-allow-methods'] = request.headers['access-control-request-method']; + } + } + } + + return h.continue; + }); + + return this.server; + } + + _extractAuthFunctionName(endpoint) { + const result = authFunctionNameExtractor(endpoint, this.serverlessLog); + + return result.unsupportedAuth ? null : result.authorizerName; + } + + _configureAuthorization(endpoint, funName, method, epath, servicePath, serviceRuntime) { + if (!endpoint.authorizer) { + return null; + } + + const authFunctionName = this._extractAuthFunctionName(endpoint); + + if (!authFunctionName) { + return null; + } + + this.serverlessLog(`Configuring Authorization: ${endpoint.path} ${authFunctionName}`); + + const authFunction = this.service.getFunction(authFunctionName); + + if (!authFunction) return this.serverlessLog(`WARNING: Authorization function ${authFunctionName} does not exist`); + + const authorizerOptions = { + resultTtlInSeconds: '300', + identitySource: 'method.request.header.Authorization', + identityValidationExpression: '(.*)', + }; + + if (typeof endpoint.authorizer === 'string') { + authorizerOptions.name = authFunctionName; + } + else { + Object.assign(authorizerOptions, endpoint.authorizer); + } + + // Create a unique scheme per endpoint + // This allows the methodArn on the event property to be set appropriately + const authKey = `${funName}-${authFunctionName}-${method}-${epath}`; + const authSchemeName = `scheme-${authKey}`; + const authStrategyName = `strategy-${authKey}`; // set strategy name for the route config + + debugLog(`Creating Authorization scheme for ${authKey}`); + + // Create the Auth Scheme for the endpoint + const scheme = createAuthScheme( + authFunction, + authorizerOptions, + authFunctionName, + epath, + this.options, + this.serverlessLog, + servicePath, + serviceRuntime, + this.serverless + ); + + // Set the auth scheme and strategy on the server + this.server.auth.scheme(authSchemeName, scheme); + this.server.auth.strategy(authStrategyName, authSchemeName); + + return authStrategyName; + } + + // All done, we can listen to incomming requests + async _listen() { + try { + await this.server.start(); + } + catch (e) { + console.error(`Unexpected error while starting serverless-offline server on port ${this.options.port}:`, e); + process.exit(1); + } + + this.printBlankLine(); + this.serverlessLog(`Offline [http] listening on http${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port}`); + } + + _createRoutes() { + let serviceRuntime = this.service.provider.runtime; + const defaultContentType = 'application/json'; + const apiKeys = this.service.provider.apiKeys; + const protectedRoutes = []; + + if (!serviceRuntime) { + throw new Error('Missing required property "runtime" for provider.'); + } + + if (typeof serviceRuntime !== 'string') { + throw new Error('Provider configuration property "runtime" wasn\'t a string.'); + } + + if (serviceRuntime === 'provided') { + if (this.options.providedRuntime) { + serviceRuntime = this.options.providedRuntime; + } + else { + throw new Error('Runtime "provided" is unsupported. Please add a --providedRuntime CLI option.'); + } + } + + if (!(serviceRuntime.startsWith('nodejs') || serviceRuntime.startsWith('python') || serviceRuntime.startsWith('ruby'))) { + this.printBlankLine(); + this.serverlessLog(`Warning: found unsupported runtime '${serviceRuntime}'`); + + return; + } + + // for simple API Key authentication model + if (apiKeys) { + this.serverlessLog(`Key with token: ${this.options.apiKey}`); + + if (this.options.noAuth) { + this.serverlessLog('Authorizers are turned off. You do not need to use x-api-key header.'); + } + else { + this.serverlessLog('Remember to use x-api-key on the request headers'); + } + } + + Object.keys(this.service.functions).forEach(key => { + + const fun = this.service.getFunction(key); + const funName = key; + const servicePath = path.join(this.serverless.config.servicePath, this.options.location); + const funOptions = functionHelper.getFunctionOptions(fun, key, servicePath, serviceRuntime); + + debugLog(`funOptions ${JSON.stringify(funOptions, null, 2)} `); + this.printBlankLine(); + debugLog(funName, 'runtime', serviceRuntime); + this.serverlessLog(`Routes for ${funName}:`); + + if (!fun.events) { + fun.events = []; + } + + // Add proxy for lamda invoke + fun.events.push({ + http: { + method: 'POST', + path: `{apiVersion}/functions/${fun.name}/invocations`, + integration: 'lambda', + request: { + template: { + // AWS SDK for NodeJS specifies as 'binary/octet-stream' not 'application/json' + 'binary/octet-stream': '$input.body', + }, + }, + response: { + headers: { + 'Content-Type': 'application/json', + }, + }, + }, + }); + + // Adds a route for each http endpoint + // eslint-disable-next-line + fun.events.forEach(event => { + + if (event.websocket) { + this.apiGatewayWebSocket._createWsAction(fun, funName, servicePath, funOptions, event); + + return; + } + if (!event.http) return; + + // Handle Simple http setup, ex. - http: GET users/index + if (typeof event.http === 'string') { + const [method, path] = event.http.split(' '); + event.http = { method, path }; + } + + // generate an enpoint via the endpoint class + const endpoint = new Endpoint(event.http, funOptions).generate(); + + const integration = endpoint.integration || 'lambda-proxy'; + const requestBodyValidationModel = (['lambda', 'lambda-proxy'].includes(integration) + ? requestBodyValidator.getModel(this.service.custom, event.http, this.serverlessLog) + : null); + const epath = endpoint.path; + const method = endpoint.method.toUpperCase(); + const requestTemplates = endpoint.requestTemplates; + + // Prefix must start and end with '/' BUT path must not end with '/' + let fullPath = this.options.prefix + (epath.startsWith('/') ? epath.slice(1) : epath); + if (fullPath !== '/' && fullPath.endsWith('/')) fullPath = fullPath.slice(0, -1); + fullPath = fullPath.replace(/\+}/g, '*}'); + + if (event.http.private) { + protectedRoutes.push(`${method}#${fullPath}`); + } + + this.serverlessLog(`${method} ${fullPath}${requestBodyValidationModel && !this.options.disableModelValidation ? ` - request body will be validated against ${requestBodyValidationModel.name}` : ''}`); + + // If the endpoint has an authorization function, create an authStrategy for the route + const authStrategyName = this.options.noAuth ? null : this._configureAuthorization(endpoint, funName, method, epath, servicePath, serviceRuntime); + + let cors = null; + if (endpoint.cors) { + cors = { + origin: endpoint.cors.origins || this.options.corsConfig.origin, + headers: endpoint.cors.headers || this.options.corsConfig.headers, + credentials: endpoint.cors.credentials || this.options.corsConfig.credentials, + exposedHeaders: this.options.corsConfig.exposedHeaders, + }; + } + + // Route creation + const routeMethod = method === 'ANY' ? '*' : method; + + const state = this.options.disableCookieValidation ? { + parse: false, + failAction: 'ignore', + } : { + parse: true, + failAction: 'error', + }; + + const routeConfig = { + cors, + auth: authStrategyName, + timeout: { socket: false }, + state, + }; + + // skip HEAD routes as hapi will fail with 'Method name not allowed: HEAD ...' + // for more details, check https://github.com/dherault/serverless-offline/issues/204 + if (routeMethod === 'HEAD') { + this.serverlessLog('HEAD method event detected. Skipping HAPI server route mapping ...'); + + return; + } + + if (routeMethod !== 'HEAD' && routeMethod !== 'GET') { + // maxBytes: Increase request size from 1MB default limit to 10MB. + // Cf AWS API GW payload limits. + routeConfig.payload = { parse: false, maxBytes: 1024 * 1024 * 10 }; + } + + this.server.route({ + method: routeMethod, + path: fullPath, + config: routeConfig, + handler: (request, h) => { // Here we go + // Payload processing + const encoding = detectEncoding(request); + + request.payload = request.payload && request.payload.toString(encoding); + request.rawPayload = request.payload; + + // Headers processing + // Hapi lowercases the headers whereas AWS does not + // so we recreate a custom headers object from the raw request + const headersArray = request.raw.req.rawHeaders; + + // During tests, `server.inject` uses *shot*, a package + // for performing injections that does not entirely mimick + // Hapi's usual request object. rawHeaders are then missing + // Hence the fallback for testing + + // Normal usage + if (headersArray) { + request.unprocessedHeaders = {}; + request.multiValueHeaders = {}; + + for (let i = 0; i < headersArray.length; i += 2) { + request.unprocessedHeaders[headersArray[i]] = headersArray[i + 1]; + request.multiValueHeaders[headersArray[i]] = (request.multiValueHeaders[headersArray[i]] || []).concat(headersArray[i + 1]); + } + } + // Lib testing + else { + request.unprocessedHeaders = request.headers; + } + + // Incomming request message + this.printBlankLine(); + this.serverlessLog(`${method} ${request.path} (λ: ${funName})`); + + // Check for APIKey + if ((protectedRoutes.includes(`${routeMethod}#${fullPath}`) || protectedRoutes.includes(`ANY#${fullPath}`)) && !this.options.noAuth) { + const errorResponse = () => h.response({ message: 'Forbidden' }).code(403).type('application/json').header('x-amzn-ErrorType', 'ForbiddenException'); + + if ('x-api-key' in request.headers) { + const requestToken = request.headers['x-api-key']; + if (requestToken !== this.options.apiKey) { + debugLog(`Method ${method} of function ${funName} token ${requestToken} not valid`); + + return errorResponse(); + } + } + else if (request.auth && request.auth.credentials && 'usageIdentifierKey' in request.auth.credentials) { + const usageIdentifierKey = request.auth.credentials.usageIdentifierKey; + if (usageIdentifierKey !== this.options.apiKey) { + debugLog(`Method ${method} of function ${funName} token ${usageIdentifierKey} not valid`); + + return errorResponse(); + } + } + else { + debugLog(`Missing x-api-key on private function ${funName}`); + + return errorResponse(); + } + } + // Shared mutable state is the root of all evil they say + const requestId = getUniqueId(); + this.requests[requestId] = { done: false }; + this.currentRequestId = requestId; + + const response = h.response(); + const contentType = request.mime || defaultContentType; + + // default request template to '' if we don't have a definition pushed in from serverless or endpoint + const requestTemplate = typeof requestTemplates !== 'undefined' && integration === 'lambda' ? requestTemplates[contentType] : ''; + + // https://hapijs.com/api#route-configuration doesn't seem to support selectively parsing + // so we have to do it ourselves + const contentTypesThatRequirePayloadParsing = ['application/json', 'application/vnd.api+json']; + if (contentTypesThatRequirePayloadParsing.includes(contentType) && request.payload && request.payload.length > 1) { + try { + if (!request.payload || request.payload.length < 1) { + request.payload = '{}'; + } + + request.payload = JSON.parse(request.payload); + } + catch (err) { + debugLog('error in converting request.payload to JSON:', err); + } + } + + debugLog('requestId:', requestId); + debugLog('contentType:', contentType); + debugLog('requestTemplate:', requestTemplate); + debugLog('payload:', request.payload); + + /* HANDLER LAZY LOADING */ + + let userHandler; // The lambda function + Object.assign(process.env, this.originalEnvironment); + + try { + if (this.options.noEnvironment) { + // This evict errors in server when we use aws services like ssm + const baseEnvironment = { + AWS_REGION: 'dev', + }; + if (!process.env.AWS_PROFILE) { + baseEnvironment.AWS_ACCESS_KEY_ID = 'dev'; + baseEnvironment.AWS_SECRET_ACCESS_KEY = 'dev'; + } + + process.env = Object.assign(baseEnvironment, process.env); + } + else { + Object.assign( + process.env, + { AWS_REGION: this.service.provider.region }, + this.service.provider.environment, + this.service.functions[key].environment + ); + } + process.env._HANDLER = fun.handler; + userHandler = functionHelper.createHandler(funOptions, this.options); + } + catch (err) { + return this._reply500(response, `Error while loading ${funName}`, err); + } + + /* REQUEST TEMPLATE PROCESSING (event population) */ + + let event = {}; + + if (integration === 'lambda') { + if (requestTemplate) { + try { + debugLog('_____ REQUEST TEMPLATE PROCESSING _____'); + // Velocity templating language parsing + const velocityContext = createVelocityContext(request, this.velocityContextOptions, request.payload || {}); + event = renderVelocityTemplateObject(requestTemplate, velocityContext); + } + catch (err) { + return this._reply500(response, `Error while parsing template "${contentType}" for ${funName}`, err); + } + } + else if (typeof request.payload === 'object') { + event = request.payload || {}; + } + } + else if (integration === 'lambda-proxy') { + event = createLambdaProxyContext(request, this.options, this.velocityContextOptions.stageVariables); + } + + event.isOffline = true; + + if (this.service.custom && this.service.custom.stageVariables) { + event.stageVariables = this.service.custom.stageVariables; + } + else if (integration !== 'lambda-proxy') { + event.stageVariables = {}; + } + + debugLog('event:', event); + + return new Promise(resolve => { + // We create the context, its callback (context.done/succeed/fail) will send the HTTP response + const lambdaContext = createLambdaContext(fun, this.service.provider, (err, data, fromPromise) => { + // Everything in this block happens once the lambda function has resolved + debugLog('_____ HANDLER RESOLVED _____'); + + // User should not call context.done twice + if (!this.requests[requestId] || this.requests[requestId].done) { + this.printBlankLine(); + const warning = fromPromise + ? `Warning: handler '${funName}' returned a promise and also uses a callback!\nThis is problematic and might cause issues in your lambda.` + : `Warning: context.done called twice within handler '${funName}'!`; + this.serverlessLog(warning); + debugLog('requestId:', requestId); + + return; + } + + this.requests[requestId].done = true; + + let result = data; + let responseName = 'default'; + const { contentHandling, responseContentType } = endpoint; + + /* RESPONSE SELECTION (among endpoint's possible responses) */ + + // Failure handling + let errorStatusCode = 0; + if (err) { + // Since the --useSeparateProcesses option loads the handler in + // a separate process and serverless-offline communicates with it + // over IPC, we are unable to catch JavaScript unhandledException errors + // when the handler code contains bad JavaScript. Instead, we "catch" + // it here and reply in the same way that we would have above when + // we lazy-load the non-IPC handler function. + if (this.options.useSeparateProcesses && err.ipcException) { + return resolve(this._reply500(response, `Error while loading ${funName}`, err)); + } + + const errorMessage = (err.message || err).toString(); + + const re = /\[(\d{3})]/; + const found = errorMessage.match(re); + if (found && found.length > 1) { + errorStatusCode = found[1]; + } + else { + errorStatusCode = '500'; + } + + // Mocks Lambda errors + result = { + errorMessage, + errorType: err.constructor.name, + stackTrace: this._getArrayStackTrace(err.stack), + }; + + this.serverlessLog(`Failure: ${errorMessage}`); + + if (result.stackTrace) { + debugLog(result.stackTrace.join('\n ')); + } + + for (const key in endpoint.responses) { + if (key !== 'default' && errorMessage.match(`^${endpoint.responses[key].selectionPattern || key}$`)) { + responseName = key; + break; + } + } + } + + debugLog(`Using response '${responseName}'`); + const chosenResponse = endpoint.responses[responseName]; + + /* RESPONSE PARAMETERS PROCCESSING */ + + const responseParameters = chosenResponse.responseParameters; + + if (responseParameters) { + + const responseParametersKeys = Object.keys(responseParameters); + + debugLog('_____ RESPONSE PARAMETERS PROCCESSING _____'); + debugLog(`Found ${responseParametersKeys.length} responseParameters for '${responseName}' response`); + + // responseParameters use the following shape: "key": "value" + Object.entries(responseParametersKeys).forEach(([key, value]) => { + + const keyArray = key.split('.'); // eg: "method.response.header.location" + const valueArray = value.split('.'); // eg: "integration.response.body.redirect.url" + + debugLog(`Processing responseParameter "${key}": "${value}"`); + + // For now the plugin only supports modifying headers + if (key.startsWith('method.response.header') && keyArray[3]) { + + const headerName = keyArray.slice(3).join('.'); + let headerValue; + debugLog('Found header in left-hand:', headerName); + + if (value.startsWith('integration.response')) { + if (valueArray[2] === 'body') { + + debugLog('Found body in right-hand'); + headerValue = (valueArray[3] ? jsonPath(result, valueArray.slice(3).join('.')) : result).toString(); + + } + else { + this.printBlankLine(); + this.serverlessLog(`Warning: while processing responseParameter "${key}": "${value}"`); + this.serverlessLog(`Offline plugin only supports "integration.response.body[.JSON_path]" right-hand responseParameter. Found "${value}" instead. Skipping.`); + this.logPluginIssue(); + this.printBlankLine(); + } + } + else { + headerValue = value.match(/^'.*'$/) ? value.slice(1, -1) : value; // See #34 + } + // Applies the header; + debugLog(`Will assign "${headerValue}" to header "${headerName}"`); + response.header(headerName, headerValue); + + } + else { + this.printBlankLine(); + this.serverlessLog(`Warning: while processing responseParameter "${key}": "${value}"`); + this.serverlessLog(`Offline plugin only supports "method.response.header.PARAM_NAME" left-hand responseParameter. Found "${key}" instead. Skipping.`); + this.logPluginIssue(); + this.printBlankLine(); + } + }); + } + + let statusCode = 200; + + if (integration === 'lambda') { + + const endpointResponseHeaders = (endpoint.response && endpoint.response.headers) || {}; + + Object.entries(endpointResponseHeaders) + .filter(([, value]) => typeof value === 'string' && /^'.*?'$/.test(value)) + .forEach(([key, value]) => response.header(key, value.slice(1, -1))); + + /* LAMBDA INTEGRATION RESPONSE TEMPLATE PROCCESSING */ + + // If there is a responseTemplate, we apply it to the result + const { responseTemplates } = chosenResponse; + + if (typeof responseTemplates === 'object') { + const responseTemplatesKeys = Object.keys(responseTemplates); + + if (responseTemplatesKeys.length) { + + // BAD IMPLEMENTATION: first key in responseTemplates + const responseTemplate = responseTemplates[responseContentType]; + + if (responseTemplate && responseTemplate !== '\n') { + + debugLog('_____ RESPONSE TEMPLATE PROCCESSING _____'); + debugLog(`Using responseTemplate '${responseContentType}'`); + + try { + const reponseContext = createVelocityContext(request, this.velocityContextOptions, result); + result = renderVelocityTemplateObject({ root: responseTemplate }, reponseContext).root; + } + catch (error) { + this.serverlessLog(`Error while parsing responseTemplate '${responseContentType}' for lambda ${funName}:`); + console.log(error.stack); + } + } + } + } + + /* LAMBDA INTEGRATION HAPIJS RESPONSE CONFIGURATION */ + + statusCode = errorStatusCode !== 0 ? errorStatusCode : (chosenResponse.statusCode || 200); + + if (!chosenResponse.statusCode) { + this.printBlankLine(); + this.serverlessLog(`Warning: No statusCode found for response "${responseName}".`); + } + + response.header('Content-Type', responseContentType, { + override: false, // Maybe a responseParameter set it already. See #34 + }); + + response.statusCode = statusCode; + + if (contentHandling === 'CONVERT_TO_BINARY') { + response.encoding = 'binary'; + response.source = Buffer.from(result, 'base64'); + response.variety = 'buffer'; + } + else { + if (result && result.body && typeof result.body !== 'string') { + return this._reply500(response, 'According to the API Gateway specs, the body content must be stringified. Check your Lambda response and make sure you are invoking JSON.stringify(YOUR_CONTENT) on your body object', {}); + } + response.source = result; + } + } + else if (integration === 'lambda-proxy') { + + /* LAMBDA PROXY INTEGRATION HAPIJS RESPONSE CONFIGURATION */ + + response.statusCode = statusCode = result.statusCode || 200; + + const headers = {}; + if (result.headers) { + Object.keys(result.headers).forEach(header => { + headers[header] = (headers[header] || []).concat(result.headers[header]); + }); + } + if (result.multiValueHeaders) { + Object.keys(result.multiValueHeaders).forEach(header => { + headers[header] = (headers[header] || []).concat(result.multiValueHeaders[header]); + }); + } + + debugLog('headers', headers); + + Object.keys(headers).forEach(header => { + if (header.toLowerCase() === 'set-cookie') { + headers[header].forEach(headerValue => { + const cookieName = headerValue.slice(0, headerValue.indexOf('=')); + const cookieValue = headerValue.slice(headerValue.indexOf('=') + 1); + h.state(cookieName, cookieValue, { encoding: 'none', strictHeader: false }); + }); + } + else { + headers[header].forEach(headerValue => { + // it looks like Hapi doesn't support multiple headers with the same name, + // appending values is the closest we can come to the AWS behavior. + response.header(header, headerValue, { append: true }); + }); + } + }); + + response.header('Content-Type', 'application/json', { override: false, duplicate: false }); + + if (typeof result.body !== 'undefined') { + if (result.isBase64Encoded) { + response.encoding = 'binary'; + response.source = Buffer.from(result.body, 'base64'); + response.variety = 'buffer'; + } + else { + if (result.body && typeof result.body !== 'string') { + return this._reply500(response, 'According to the API Gateway specs, the body content must be stringified. Check your Lambda response and make sure you are invoking JSON.stringify(YOUR_CONTENT) on your body object', {}); + } + response.source = result.body; + } + } + } + + // Log response + let whatToLog = result; + + try { + whatToLog = JSON.stringify(result); + } + catch (error) { + // nothing + } + finally { + if (this.options.printOutput) this.serverlessLog(err ? `Replying ${statusCode}` : `[${statusCode}] ${whatToLog}`); + debugLog('requestId:', requestId); + } + + // Bon voyage! + resolve(response); + }); + + // Now we are outside of createLambdaContext, so this happens before the handler gets called: + + // We cannot use Hapijs's timeout feature because the logic above can take a significant time, so we implement it ourselves + this.requests[requestId].timeout = this.options.noTimeout ? null : setTimeout( + this._replyTimeout.bind(this, response, resolve, funName, funOptions.funTimeout, requestId), + funOptions.funTimeout + ); + + // If request body validation is enabled, validate body against the request model. + if (requestBodyValidationModel && !this.options.disableModelValidation) { + try { + requestBodyValidator.validate(requestBodyValidationModel, event.body); + } + catch (error) { + // When request body validation fails, APIG will return back 400 as detailed in: + // https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-method-request-validation.html + return resolve(this._replyError(400, response, `Invalid request body for '${funName}' handler`, error)); + } + } + + // Finally we call the handler + debugLog('_____ CALLING HANDLER _____'); + + const cleanup = () => { + this._clearTimeout(requestId); + delete this.requests[requestId]; + }; + + let x; + + if (this.options.showDuration) { + performance.mark(`${requestId}-start`); + + const obs = new PerformanceObserver(list => { + for (const entry of list.getEntries()) { + this.serverlessLog(`Duration ${entry.duration.toFixed(2)} ms (λ: ${entry.name})`); + } + + obs.disconnect(); + }); + + obs.observe({ entryTypes: ['measure'] }); + } + + try { + x = userHandler(event, lambdaContext, (err, result) => { + setTimeout(cleanup, 0); + + if (this.options.showDuration) { + performance.mark(`${requestId}-end`); + performance.measure(funName, `${requestId}-start`, `${requestId}-end`); + } + + return lambdaContext.done(err, result); + }); + + // Promise support + if (!this.requests[requestId].done) { + if (x && typeof x.then === 'function') { + x.then(lambdaContext.succeed).catch(lambdaContext.fail).then(cleanup, cleanup); + } + else if (x instanceof Error) { + lambdaContext.fail(x); + } + } + } + catch (error) { + cleanup(); + + return resolve(this._reply500(response, `Uncaught error in your '${funName}' handler`, error)); + } + }); + }, + }); + }); + }); + } + + // Bad news + _replyError(responseCode, response, message, err) { + const stackTrace = this._getArrayStackTrace(err.stack); + + this.serverlessLog(message); + if (stackTrace && stackTrace.length > 0) { + console.log(stackTrace); + } + else { + console.log(err); + } + + response.header('Content-Type', 'application/json'); + + /* eslint-disable no-param-reassign */ + response.statusCode = responseCode; + response.source = { + errorMessage: message, + errorType: err.constructor.name, + stackTrace, + offlineInfo: 'If you believe this is an issue with the plugin please submit it, thanks. https://github.com/dherault/serverless-offline/issues', + }; + /* eslint-enable no-param-reassign */ + this.serverlessLog('Replying error in handler'); + + return response; + } + + _reply500(response, message, err) { + // APIG replies 200 by default on failures + return this._replyError(200, response, message, err); + } + + _replyTimeout(response, resolve, funName, funTimeout, requestId) { + if (this.currentRequestId !== requestId) return; + + this.serverlessLog(`Replying timeout after ${funTimeout}ms`); + /* eslint-disable no-param-reassign */ + response.statusCode = 503; + response.source = `[Serverless-Offline] Your λ handler '${funName}' timed out after ${funTimeout}ms.`; + /* eslint-enable no-param-reassign */ + resolve(response); + } + + _clearTimeout(requestId) { + const { timeout } = this.requests[requestId] || {}; + clearTimeout(timeout); + } + + _createResourceRoutes() { + if (!this.options.resourceRoutes) return true; + const resourceRoutesOptions = this.options.resourceRoutes; + const resourceRoutes = parseResources(this.service.resources); + + if (!resourceRoutes || !Object.keys(resourceRoutes).length) return true; + + this.printBlankLine(); + this.serverlessLog('Routes defined in resources:'); + + Object.entries(resourceRoutes).forEach(([methodId, resourceRoutesObj]) => { + const { isProxy, method, path, pathResource, proxyUri } = resourceRoutesObj; + + if (!isProxy) { + return this.serverlessLog(`WARNING: Only HTTP_PROXY is supported. Path '${pathResource}' is ignored.`); + } + if (!path) { + return this.serverlessLog(`WARNING: Could not resolve path for '${methodId}'.`); + } + + let fullPath = this.options.prefix + (pathResource.startsWith('/') ? pathResource.slice(1) : pathResource); + if (fullPath !== '/' && fullPath.endsWith('/')) fullPath = fullPath.slice(0, -1); + fullPath = fullPath.replace(/\+}/g, '*}'); + + const proxyUriOverwrite = resourceRoutesOptions[methodId] || {}; + const proxyUriInUse = proxyUriOverwrite.Uri || proxyUri; + + if (!proxyUriInUse) { + return this.serverlessLog(`WARNING: Could not load Proxy Uri for '${methodId}'`); + } + + const routeMethod = method === 'ANY' ? '*' : method; + const routeConfig = { cors: this.options.corsConfig }; + + // skip HEAD routes as hapi will fail with 'Method name not allowed: HEAD ...' + // for more details, check https://github.com/dherault/serverless-offline/issues/204 + if (routeMethod === 'HEAD') { + this.serverlessLog('HEAD method event detected. Skipping HAPI server route mapping ...'); + + return; + } + + if (routeMethod !== 'HEAD' && routeMethod !== 'GET') { + routeConfig.payload = { parse: false }; + } + + this.serverlessLog(`${method} ${fullPath} -> ${proxyUriInUse}`); + this.server.route({ + method: routeMethod, + path: fullPath, + config: routeConfig, + handler: (request, h) => { + const { params } = request; + let resultUri = proxyUriInUse; + + Object.entries(params).forEach(([key, value]) => { + resultUri = resultUri.replace(`{${key}}`, value); + }); + + if (request.url.search !== null) { + resultUri += request.url.search; // search is empty string by default + } + + this.serverlessLog(`PROXY ${request.method} ${request.url.path} -> ${resultUri}`); + + return h.proxy({ uri: resultUri, passThrough: true }); + }, + }); + }); + } + + _create404Route() { + // If a {proxy+} route exists, don't conflict with it + if (this.server.match('*', '/{p*}')) return; + + this.server.route({ + method: '*', + path: '/{p*}', + config: { cors: this.options.corsConfig }, + handler: (request, h) => { + const response = h.response({ + statusCode: 404, + error: 'Serverless-offline: route not found.', + currentRoute: `${request.method} - ${request.path}`, + existingRoutes: this.server.table() + .filter(route => route.path !== '/{p*}') // Exclude this (404) route + .sort((a, b) => a.path <= b.path ? -1 : 1) // Sort by path + .map(route => `${route.method} - ${route.path}`), // Human-friendly result + }); + response.statusCode = 404; + + return response; + }, + }); + } + + _getArrayStackTrace(stack) { + if (!stack) return null; + + const splittedStack = stack.split('\n'); + + return splittedStack.slice(0, splittedStack.findIndex(item => item.match(/server.route.handler.createLambdaContext/))).map(line => line.trim()); + } +}; diff --git a/src/ApiGatewayWebSocket.js b/src/ApiGatewayWebSocket.js new file mode 100644 index 000000000..b566f3867 --- /dev/null +++ b/src/ApiGatewayWebSocket.js @@ -0,0 +1,344 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const hapi = require('@hapi/hapi'); +const h2o2 = require('@hapi/h2o2'); +const debugLog = require('./debugLog'); +const createAuthScheme = require('./createAuthScheme'); +const functionHelper = require('./functionHelper'); +const { getUniqueId } = require('./utils'); +const authFunctionNameExtractor = require('./authFunctionNameExtractor'); +const wsHelpers = require('./websocketHelpers'); + +module.exports = class ApiGatewayWebSocket { + constructor(serverless, options) { + this.serverless = serverless; + this.service = serverless.service; + this.serverlessLog = serverless.cli.log.bind(serverless.cli); + this.options = options; + this.exitCode = 0; + this.clients = new Map(); + this.wsActions = {}; + this.websocketsApiRouteSelectionExpression = serverless.service.provider.websocketsApiRouteSelectionExpression || '$request.body.action'; + } + + printBlankLine() { + console.log(); + } + + logPluginIssue() { + this.serverlessLog('If you think this is an issue with the plugin please submit it, thanks!'); + this.serverlessLog('https://github.com/dherault/serverless-offline/issues'); + } + + _createWebSocket() { + // start COPY PASTE FROM HTTP SERVER CODE + const serverOptions = { + host: this.options.host, + port: this.options.wsPort, + router: { + stripTrailingSlash: !this.options.preserveTrailingSlash, // removes trailing slashes on incoming paths. + }, + }; + + const httpsDir = this.options.httpsProtocol; + + // HTTPS support + if (typeof httpsDir === 'string' && httpsDir.length > 0) { + serverOptions.tls = { + key: fs.readFileSync(path.resolve(httpsDir, 'key.pem'), 'ascii'), + cert: fs.readFileSync(path.resolve(httpsDir, 'cert.pem'), 'ascii'), + }; + } + + serverOptions.state = this.options.enforceSecureCookies ? { + isHttpOnly: true, + isSecure: true, + isSameSite: false, + } : { + isHttpOnly: false, + isSecure: false, + isSameSite: false, + }; + + // Hapijs server creation + this.wsServer = hapi.server(serverOptions); + + this.wsServer.register(h2o2).catch(err => err && this.serverlessLog(err)); + + // Enable CORS preflight response + this.wsServer.ext('onPreResponse', (request, h) => { + if (request.headers.origin) { + const response = request.response.isBoom ? request.response.output : request.response; + + response.headers['access-control-allow-origin'] = request.headers.origin; + response.headers['access-control-allow-credentials'] = 'true'; + + if (request.method === 'options') { + response.statusCode = 200; + response.headers['access-control-expose-headers'] = 'content-type, content-length, etag'; + response.headers['access-control-max-age'] = 60 * 10; + + if (request.headers['access-control-request-headers']) { + response.headers['access-control-allow-headers'] = request.headers['access-control-request-headers']; + } + + if (request.headers['access-control-request-method']) { + response.headers['access-control-allow-methods'] = request.headers['access-control-request-method']; + } + } + } + + return h.continue; + }); + // end COPY PASTE FROM HTTP SERVER CODE + + this.wsServer.register(require('hapi-plugin-websocket')).catch(err => err && this.serverlessLog(err)); + + const doAction = (ws, connectionId, name, event, context, doDeafultAction/* , onError */) => { + const sendError = err => { + if (ws.readyState === /* OPEN */1) ws.send(JSON.stringify({ message:'Internal server error', connectionId, requestId:'1234567890' })); + debugLog(`Error in handler of action ${action}`, err); + }; + let action = this.wsActions[name]; + if (!action && doDeafultAction) action = this.wsActions.$default; + if (!action) return; + let p = null; + try { + p = action.handler(event, context, err => { + if (!err) return; + sendError(err); + }); + } + catch (err) { + sendError(err); + } + + if (p) { + p.catch(err => { + sendError(err); + }); + } + }; + + this.wsServer.route({ + method: 'POST', + path: '/', + config: { + payload: { output: 'data', parse: true, allow: 'application/json' }, + plugins: { + websocket: { + only: true, + initially: false, + connect: ({ ws, req }) => { + const parseQuery = queryString => { + const query = {}; const parts = queryString.split('?'); + if (parts.length < 2) return {}; + const pairs = parts[1].split('&'); + pairs.forEach(pair => { + const kv = pair.split('='); + query[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1] || ''); + }); + + return query; + }; + + const queryStringParameters = parseQuery(req.url); + const connection = { connectionId:getUniqueId(), connectionTime:Date.now() }; + debugLog(`connect:${connection.connectionId}`); + + this.clients.set(ws, connection); + let event = wsHelpers.createConnectEvent('$connect', 'CONNECT', connection, this.options); + if (Object.keys(queryStringParameters).length > 0) event = { queryStringParameters, ...event }; + const context = wsHelpers.createContext('$connect'); + + doAction(ws, connection.connectionId, '$connect', event, context); + }, + disconnect: ({ ws }) => { + const connection = this.clients.get(ws); + debugLog(`disconnect:${connection.connectionId}`); + this.clients.delete(ws); + const event = wsHelpers.createDisconnectEvent('$disconnect', 'DISCONNECT', connection, this.options); + const context = wsHelpers.createContext('$disconnect'); + + doAction(ws, connection.connectionId, '$disconnect', event, context); + }, + }, + }, + }, + + handler: (request, h) => { + const { initially, ws } = request.websocket(); + if (!request.payload || initially) return h.response().code(204); + const connection = this.clients.get(ws); + let actionName = null; + if (this.websocketsApiRouteSelectionExpression.startsWith('$request.body.')) { + actionName = request.payload; + if (typeof actionName === 'object') { + this.websocketsApiRouteSelectionExpression.replace('$request.body.', '').split('.').forEach(key => { + if (actionName) actionName = actionName[key]; + }); + } + else actionName = null; + } + if (typeof actionName !== 'string') actionName = null; + const action = actionName || '$default'; + debugLog(`action:${action} on connection=${connection.connectionId}`); + const event = wsHelpers.createEvent(action, 'MESSAGE', connection, request.payload, this.options); + const context = wsHelpers.createContext(action); + + doAction(ws, connection.connectionId, action, event, context, true); + + return h.response().code(204); + }, + }); + + this.wsServer.route({ + method: 'GET', + path: '/{path*}', + handler: (request, h) => h.response().code(426), + }); + + this.wsServer.route({ + method: 'POST', + path: '/@connections/{connectionId}', + config: { payload: { parse: false } }, + handler: (request, h) => { + debugLog(`got POST to ${request.url}`); + const getByConnectionId = (map, searchValue) => { + for (const [key, connection] of map.entries()) { + if (connection.connectionId === searchValue) return key; + } + + return undefined; + }; + + const ws = getByConnectionId(this.clients, request.params.connectionId); + if (!ws) return h.response().code(410); + if (!request.payload) return ''; + ws.send(request.payload.toString()); + // console.log(`sent "${request.payload.toString().substring}" to ${request.params.connectionId}`); + debugLog(`sent data to connection:${request.params.connectionId}`); + + return ''; + }, + }); + } + + _createWsAction(fun, funName, servicePath, funOptions, event) { + let handler; // The lambda function + Object.assign(process.env, this.originalEnvironment); + + try { + if (this.options.noEnvironment) { + // This evict errors in server when we use aws services like ssm + const baseEnvironment = { + AWS_REGION: 'dev', + }; + if (!process.env.AWS_PROFILE) { + baseEnvironment.AWS_ACCESS_KEY_ID = 'dev'; + baseEnvironment.AWS_SECRET_ACCESS_KEY = 'dev'; + } + + process.env = Object.assign(baseEnvironment, process.env); + } + else { + Object.assign( + process.env, + { AWS_REGION: this.service.provider.region }, + this.service.provider.environment, + this.service.functions[funName].environment + ); + } + process.env._HANDLER = fun.handler; + handler = functionHelper.createHandler(funOptions, this.options); + } + catch (err) { + return this.serverlessLog(`Error while loading ${funName}`, err); + } + + const actionName = event.websocket.route; + const action = { funName, fun, funOptions, servicePath, handler }; + this.wsActions[actionName] = action; + this.serverlessLog(`Action '${event.websocket.route}'`); + } + + _extractAuthFunctionName(endpoint) { + const result = authFunctionNameExtractor(endpoint, this.serverlessLog); + + return result.unsupportedAuth ? null : result.authorizerName; + } + + _configureAuthorization(endpoint, funName, method, epath, servicePath, serviceRuntime) { + if (!endpoint.authorizer) { + return null; + } + + const authFunctionName = this._extractAuthFunctionName(endpoint); + + if (!authFunctionName) { + return null; + } + + this.serverlessLog(`Configuring Authorization: ${endpoint.path} ${authFunctionName}`); + + const authFunction = this.service.getFunction(authFunctionName); + + if (!authFunction) return this.serverlessLog(`WARNING: Authorization function ${authFunctionName} does not exist`); + + const authorizerOptions = { + resultTtlInSeconds: '300', + identitySource: 'method.request.header.Authorization', + identityValidationExpression: '(.*)', + }; + + if (typeof endpoint.authorizer === 'string') { + authorizerOptions.name = authFunctionName; + } + else { + Object.assign(authorizerOptions, endpoint.authorizer); + } + + // Create a unique scheme per endpoint + // This allows the methodArn on the event property to be set appropriately + const authKey = `${funName}-${authFunctionName}-${method}-${epath}`; + const authSchemeName = `scheme-${authKey}`; + const authStrategyName = `strategy-${authKey}`; // set strategy name for the route config + + debugLog(`Creating Authorization scheme for ${authKey}`); + + // Create the Auth Scheme for the endpoint + const scheme = createAuthScheme( + authFunction, + authorizerOptions, + authFunctionName, + epath, + this.options, + this.serverlessLog, + servicePath, + serviceRuntime, + this.serverless + ); + + // Set the auth scheme and strategy on the server + this.server.auth.scheme(authSchemeName, scheme); + this.server.auth.strategy(authStrategyName, authSchemeName); + + return authStrategyName; + } + + // All done, we can listen to incomming requests + async _listen() { + try { + await this.wsServer.start(); + } + catch (e) { + console.error(`Unexpected error while starting serverless-offline websocket server on port ${this.options.wsPort}:`, e); + process.exit(1); + } + + this.printBlankLine(); + this.serverlessLog(`Offline [websocket] listening on ws${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.wsPort}`); + } +}; diff --git a/src/index.js b/src/index.js index 9cc349679..190b88e75 100755 --- a/src/index.js +++ b/src/index.js @@ -1,37 +1,13 @@ 'use strict'; -// Node dependencies -const fs = require('fs'); -const path = require('path'); -const { performance, PerformanceObserver } = require('perf_hooks'); const { exec } = require('child_process'); - -// External dependencies -const hapi = require('@hapi/hapi'); -const h2o2 = require('@hapi/h2o2'); - -// Internal lib +const ApiGateway = require('./ApiGateway'); +const ApiGatewayWebSocket = require('./ApiGatewayWebSocket'); const debugLog = require('./debugLog'); -const jsonPath = require('./jsonPath'); -const createLambdaContext = require('./createLambdaContext'); -const createVelocityContext = require('./createVelocityContext'); -const createLambdaProxyContext = require('./createLambdaProxyContext'); -const renderVelocityTemplateObject = require('./renderVelocityTemplateObject'); -const createAuthScheme = require('./createAuthScheme'); const functionHelper = require('./functionHelper'); -const Endpoint = require('./Endpoint'); -const parseResources = require('./parseResources'); -const { createDefaultApiKey, detectEncoding, getUniqueId } = require('./utils'); -const authFunctionNameExtractor = require('./authFunctionNameExtractor'); -const requestBodyValidator = require('./requestBodyValidator'); -const wsHelpers = require('./websocketHelpers'); - -/* - I'm against monolithic code like this file - but splitting it induces unneeded complexity. -*/ -class Offline { +const { createDefaultApiKey } = require('./utils'); +module.exports = class Offline { constructor(serverless, options) { this.serverless = serverless; this.service = serverless.service; @@ -39,8 +15,6 @@ class Offline { this.options = options; this.exitCode = 0; this.clients = new Map(); - this.wsActions = {}; - this.websocketsApiRouteSelectionExpression = serverless.service.provider.websocketsApiRouteSelectionExpression || '$request.body.action'; this.commands = { offline: { @@ -178,7 +152,8 @@ class Offline { process.env.IS_OFFLINE = true; return Promise.resolve(this._buildServer()) - .then(() => this._listen()) + .then(() => this.apiGateway._listen()) + .then(() => this.apiGatewayWebSocket._listen()) .then(() => this.options.exec ? this._executeShellScript() : this._listenForTermination()); } @@ -241,19 +216,20 @@ class Offline { } _buildServer() { - // Maps a request id to the request's state (done: bool, timeout: timer) - this.requests = {}; - - // Methods this._setOptions(); // Will create meaningful options from cli options this._storeOriginalEnvironment(); // stores the original process.env for assigning upon invoking the handlers - this._createServer(); // Hapijs boot - this._createRoutes(); // API Gateway emulation - this._createResourceRoutes(); // HTTP Proxy defined in Resource - this._create404Route(); // Not found handling - this._createWebSocket(); - return this.server; + this.apiGateway = new ApiGateway(this.serverless, this.options, this.velocityContextOptions); + this.apiGatewayWebSocket = new ApiGatewayWebSocket(this.serverless, this.options); + + const server = this.apiGateway._createServer(); + this.apiGatewayWebSocket._createWebSocket(); + + this.apiGateway._createRoutes(); // API Gateway emulation + this.apiGateway._createResourceRoutes(); // HTTP Proxy defined in Resource + this.apiGateway._create404Route(); // Not found handling + + return server; } _storeOriginalEnvironment() { @@ -330,1236 +306,14 @@ class Offline { debugLog('options:', this.options); } - _createServer() { - const serverOptions = { - host: this.options.host, - port: this.options.port, - router: { - stripTrailingSlash: !this.options.preserveTrailingSlash, // removes trailing slashes on incoming paths. - }, - }; - - const httpsDir = this.options.httpsProtocol; - - // HTTPS support - if (typeof httpsDir === 'string' && httpsDir.length > 0) { - serverOptions.tls = { - key: fs.readFileSync(path.resolve(httpsDir, 'key.pem'), 'ascii'), - cert: fs.readFileSync(path.resolve(httpsDir, 'cert.pem'), 'ascii'), - }; - } - - serverOptions.state = this.options.enforceSecureCookies ? { - isHttpOnly: true, - isSecure: true, - isSameSite: false, - } : { - isHttpOnly: false, - isSecure: false, - isSameSite: false, - }; - - // Hapijs server creation - this.server = hapi.server(serverOptions); - - this.server.register(h2o2).catch(err => err && this.serverlessLog(err)); - - // Enable CORS preflight response - this.server.ext('onPreResponse', (request, h) => { - if (request.headers.origin) { - const response = request.response.isBoom ? request.response.output : request.response; - - response.headers['access-control-allow-origin'] = request.headers.origin; - response.headers['access-control-allow-credentials'] = 'true'; - - if (request.method === 'options') { - response.statusCode = 200; - response.headers['access-control-expose-headers'] = 'content-type, content-length, etag'; - response.headers['access-control-max-age'] = 60 * 10; - - if (request.headers['access-control-request-headers']) { - response.headers['access-control-allow-headers'] = request.headers['access-control-request-headers']; - } - - if (request.headers['access-control-request-method']) { - response.headers['access-control-allow-methods'] = request.headers['access-control-request-method']; - } - } - } - - return h.continue; - }); - } - - _createWebSocket() { - // start COPY PASTE FROM HTTP SERVER CODE - const serverOptions = { - host: this.options.host, - port: this.options.wsPort, - router: { - stripTrailingSlash: !this.options.preserveTrailingSlash, // removes trailing slashes on incoming paths. - }, - }; - - const httpsDir = this.options.httpsProtocol; - - // HTTPS support - if (typeof httpsDir === 'string' && httpsDir.length > 0) { - serverOptions.tls = { - key: fs.readFileSync(path.resolve(httpsDir, 'key.pem'), 'ascii'), - cert: fs.readFileSync(path.resolve(httpsDir, 'cert.pem'), 'ascii'), - }; - } - - serverOptions.state = this.options.enforceSecureCookies ? { - isHttpOnly: true, - isSecure: true, - isSameSite: false, - } : { - isHttpOnly: false, - isSecure: false, - isSameSite: false, - }; - - // Hapijs server creation - this.wsServer = hapi.server(serverOptions); - - this.wsServer.register(h2o2).catch(err => err && this.serverlessLog(err)); - - // Enable CORS preflight response - this.wsServer.ext('onPreResponse', (request, h) => { - if (request.headers.origin) { - const response = request.response.isBoom ? request.response.output : request.response; - - response.headers['access-control-allow-origin'] = request.headers.origin; - response.headers['access-control-allow-credentials'] = 'true'; - - if (request.method === 'options') { - response.statusCode = 200; - response.headers['access-control-expose-headers'] = 'content-type, content-length, etag'; - response.headers['access-control-max-age'] = 60 * 10; - - if (request.headers['access-control-request-headers']) { - response.headers['access-control-allow-headers'] = request.headers['access-control-request-headers']; - } - - if (request.headers['access-control-request-method']) { - response.headers['access-control-allow-methods'] = request.headers['access-control-request-method']; - } - } - } - - return h.continue; - }); - // end COPY PASTE FROM HTTP SERVER CODE - - this.wsServer.register(require('hapi-plugin-websocket')).catch(err => err && this.serverlessLog(err)); - - const doAction = (ws, connectionId, name, event, context, doDeafultAction/* , onError */) => { - const sendError = err => { - if (ws.readyState === /* OPEN */1) ws.send(JSON.stringify({ message:'Internal server error', connectionId, requestId:'1234567890' })); - debugLog(`Error in handler of action ${action}`, err); - }; - let action = this.wsActions[name]; - if (!action && doDeafultAction) action = this.wsActions.$default; - if (!action) return; - let p = null; - try { - p = action.handler(event, context, err => { - if (!err) return; - sendError(err); - }); - } - catch (err) { - sendError(err); - } - - if (p) { - p.catch(err => { - sendError(err); - }); - } - }; - - this.wsServer.route({ - method: 'POST', - path: '/', - config: { - payload: { output: 'data', parse: true, allow: 'application/json' }, - plugins: { - websocket: { - only: true, - initially: false, - connect: ({ ws, req }) => { - const parseQuery = queryString => { - const query = {}; const parts = queryString.split('?'); - if (parts.length < 2) return {}; - const pairs = parts[1].split('&'); - pairs.forEach(pair => { - const kv = pair.split('='); - query[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1] || ''); - }); - - return query; - }; - - const queryStringParameters = parseQuery(req.url); - const connection = { connectionId:getUniqueId(), connectionTime:Date.now() }; - debugLog(`connect:${connection.connectionId}`); - - this.clients.set(ws, connection); - let event = wsHelpers.createConnectEvent('$connect', 'CONNECT', connection, this.options); - if (Object.keys(queryStringParameters).length > 0) event = { queryStringParameters, ...event }; - const context = wsHelpers.createContext('$connect'); - - doAction(ws, connection.connectionId, '$connect', event, context); - }, - disconnect: ({ ws }) => { - const connection = this.clients.get(ws); - debugLog(`disconnect:${connection.connectionId}`); - this.clients.delete(ws); - const event = wsHelpers.createDisconnectEvent('$disconnect', 'DISCONNECT', connection, this.options); - const context = wsHelpers.createContext('$disconnect'); - - doAction(ws, connection.connectionId, '$disconnect', event, context); - }, - }, - }, - }, - - handler: (request, h) => { - const { initially, ws } = request.websocket(); - if (!request.payload || initially) return h.response().code(204); - const connection = this.clients.get(ws); - let actionName = null; - if (this.websocketsApiRouteSelectionExpression.startsWith('$request.body.')) { - actionName = request.payload; - if (typeof actionName === 'object') { - this.websocketsApiRouteSelectionExpression.replace('$request.body.', '').split('.').forEach(key => { - if (actionName) actionName = actionName[key]; - }); - } - else actionName = null; - } - if (typeof actionName !== 'string') actionName = null; - const action = actionName || '$default'; - debugLog(`action:${action} on connection=${connection.connectionId}`); - const event = wsHelpers.createEvent(action, 'MESSAGE', connection, request.payload, this.options); - const context = wsHelpers.createContext(action); - - doAction(ws, connection.connectionId, action, event, context, true); - - return h.response().code(204); - }, - }); - - this.wsServer.route({ - method: 'GET', - path: '/{path*}', - handler: (request, h) => h.response().code(426), - }); - - this.wsServer.route({ - method: 'POST', - path: '/@connections/{connectionId}', - config: { payload: { parse: false } }, - handler: (request, h) => { - debugLog(`got POST to ${request.url}`); - const getByConnectionId = (map, searchValue) => { - for (const [key, connection] of map.entries()) { - if (connection.connectionId === searchValue) return key; - } - - return undefined; - }; - - const ws = getByConnectionId(this.clients, request.params.connectionId); - if (!ws) return h.response().code(410); - if (!request.payload) return ''; - ws.send(request.payload.toString()); - // console.log(`sent "${request.payload.toString().substring}" to ${request.params.connectionId}`); - debugLog(`sent data to connection:${request.params.connectionId}`); - - return ''; - }, - }); - } - - _createWsAction(fun, funName, servicePath, funOptions, event) { - let handler; // The lambda function - Object.assign(process.env, this.originalEnvironment); - - try { - if (this.options.noEnvironment) { - // This evict errors in server when we use aws services like ssm - const baseEnvironment = { - AWS_REGION: 'dev', - }; - if (!process.env.AWS_PROFILE) { - baseEnvironment.AWS_ACCESS_KEY_ID = 'dev'; - baseEnvironment.AWS_SECRET_ACCESS_KEY = 'dev'; - } - - process.env = Object.assign(baseEnvironment, process.env); - } - else { - Object.assign( - process.env, - { AWS_REGION: this.service.provider.region }, - this.service.provider.environment, - this.service.functions[funName].environment - ); - } - process.env._HANDLER = fun.handler; - handler = functionHelper.createHandler(funOptions, this.options); - } - catch (err) { - return this.serverlessLog(`Error while loading ${funName}`, err); - } - - const actionName = event.websocket.route; - const action = { funName, fun, funOptions, servicePath, handler }; - this.wsActions[actionName] = action; - this.serverlessLog(`Action '${event.websocket.route}'`); - } - - _createRoutes() { - let serviceRuntime = this.service.provider.runtime; - const defaultContentType = 'application/json'; - const apiKeys = this.service.provider.apiKeys; - const protectedRoutes = []; - - if (!serviceRuntime) { - throw new Error('Missing required property "runtime" for provider.'); - } - - if (typeof serviceRuntime !== 'string') { - throw new Error('Provider configuration property "runtime" wasn\'t a string.'); - } - - if (serviceRuntime === 'provided') { - if (this.options.providedRuntime) { - serviceRuntime = this.options.providedRuntime; - } - else { - throw new Error('Runtime "provided" is unsupported. Please add a --providedRuntime CLI option.'); - } - } - - if (!(serviceRuntime.startsWith('nodejs') || serviceRuntime.startsWith('python') || serviceRuntime.startsWith('ruby'))) { - this.printBlankLine(); - this.serverlessLog(`Warning: found unsupported runtime '${serviceRuntime}'`); - - return; - } - - // for simple API Key authentication model - if (apiKeys) { - this.serverlessLog(`Key with token: ${this.options.apiKey}`); - - if (this.options.noAuth) { - this.serverlessLog('Authorizers are turned off. You do not need to use x-api-key header.'); - } - else { - this.serverlessLog('Remember to use x-api-key on the request headers'); - } - } - - Object.keys(this.service.functions).forEach(key => { - - const fun = this.service.getFunction(key); - const funName = key; - const servicePath = path.join(this.serverless.config.servicePath, this.options.location); - const funOptions = functionHelper.getFunctionOptions(fun, key, servicePath, serviceRuntime); - - debugLog(`funOptions ${JSON.stringify(funOptions, null, 2)} `); - this.printBlankLine(); - debugLog(funName, 'runtime', serviceRuntime); - this.serverlessLog(`Routes for ${funName}:`); - - if (!fun.events) { - fun.events = []; - } - - // Add proxy for lamda invoke - fun.events.push({ - http: { - method: 'POST', - path: `{apiVersion}/functions/${fun.name}/invocations`, - integration: 'lambda', - request: { - template: { - // AWS SDK for NodeJS specifies as 'binary/octet-stream' not 'application/json' - 'binary/octet-stream': '$input.body', - }, - }, - response: { - headers: { - 'Content-Type': 'application/json', - }, - }, - }, - }); - // Adds a route for each http endpoint - // eslint-disable-next-line - fun.events.forEach(event => { - if (event.websocket) { - this._createWsAction(fun, funName, servicePath, funOptions, event); - - return; - } - if (!event.http) return; - - // Handle Simple http setup, ex. - http: GET users/index - if (typeof event.http === 'string') { - const [method, path] = event.http.split(' '); - event.http = { method, path }; - } - - // generate an enpoint via the endpoint class - const endpoint = new Endpoint(event.http, funOptions).generate(); - - const integration = endpoint.integration || 'lambda-proxy'; - const requestBodyValidationModel = (['lambda', 'lambda-proxy'].includes(integration) - ? requestBodyValidator.getModel(this.service.custom, event.http, this.serverlessLog) - : null); - const epath = endpoint.path; - const method = endpoint.method.toUpperCase(); - const requestTemplates = endpoint.requestTemplates; - - // Prefix must start and end with '/' BUT path must not end with '/' - let fullPath = this.options.prefix + (epath.startsWith('/') ? epath.slice(1) : epath); - if (fullPath !== '/' && fullPath.endsWith('/')) fullPath = fullPath.slice(0, -1); - fullPath = fullPath.replace(/\+}/g, '*}'); - - if (event.http.private) { - protectedRoutes.push(`${method}#${fullPath}`); - } - - this.serverlessLog(`${method} ${fullPath}${requestBodyValidationModel && !this.options.disableModelValidation ? ` - request body will be validated against ${requestBodyValidationModel.name}` : ''}`); - - // If the endpoint has an authorization function, create an authStrategy for the route - const authStrategyName = this.options.noAuth ? null : this._configureAuthorization(endpoint, funName, method, epath, servicePath, serviceRuntime); - - let cors = null; - if (endpoint.cors) { - cors = { - origin: endpoint.cors.origins || this.options.corsConfig.origin, - headers: endpoint.cors.headers || this.options.corsConfig.headers, - credentials: endpoint.cors.credentials || this.options.corsConfig.credentials, - exposedHeaders: this.options.corsConfig.exposedHeaders, - }; - } - - // Route creation - const routeMethod = method === 'ANY' ? '*' : method; - - const state = this.options.disableCookieValidation ? { - parse: false, - failAction: 'ignore', - } : { - parse: true, - failAction: 'error', - }; - - const routeConfig = { - cors, - auth: authStrategyName, - timeout: { socket: false }, - state, - }; - - // skip HEAD routes as hapi will fail with 'Method name not allowed: HEAD ...' - // for more details, check https://github.com/dherault/serverless-offline/issues/204 - if (routeMethod === 'HEAD') { - this.serverlessLog('HEAD method event detected. Skipping HAPI server route mapping ...'); - - return; - } - - if (routeMethod !== 'HEAD' && routeMethod !== 'GET') { - // maxBytes: Increase request size from 1MB default limit to 10MB. - // Cf AWS API GW payload limits. - routeConfig.payload = { parse: false, maxBytes: 1024 * 1024 * 10 }; - } - - this.server.route({ - method: routeMethod, - path: fullPath, - config: routeConfig, - handler: (request, h) => { // Here we go - // Payload processing - const encoding = detectEncoding(request); - - request.payload = request.payload && request.payload.toString(encoding); - request.rawPayload = request.payload; - - // Headers processing - // Hapi lowercases the headers whereas AWS does not - // so we recreate a custom headers object from the raw request - const headersArray = request.raw.req.rawHeaders; - - // During tests, `server.inject` uses *shot*, a package - // for performing injections that does not entirely mimick - // Hapi's usual request object. rawHeaders are then missing - // Hence the fallback for testing - - // Normal usage - if (headersArray) { - request.unprocessedHeaders = {}; - request.multiValueHeaders = {}; - - for (let i = 0; i < headersArray.length; i += 2) { - request.unprocessedHeaders[headersArray[i]] = headersArray[i + 1]; - request.multiValueHeaders[headersArray[i]] = (request.multiValueHeaders[headersArray[i]] || []).concat(headersArray[i + 1]); - } - } - // Lib testing - else { - request.unprocessedHeaders = request.headers; - } - - // Incomming request message - this.printBlankLine(); - this.serverlessLog(`${method} ${request.path} (λ: ${funName})`); - - // Check for APIKey - if ((protectedRoutes.includes(`${routeMethod}#${fullPath}`) || protectedRoutes.includes(`ANY#${fullPath}`)) && !this.options.noAuth) { - const errorResponse = () => h.response({ message: 'Forbidden' }).code(403).type('application/json').header('x-amzn-ErrorType', 'ForbiddenException'); - - if ('x-api-key' in request.headers) { - const requestToken = request.headers['x-api-key']; - if (requestToken !== this.options.apiKey) { - debugLog(`Method ${method} of function ${funName} token ${requestToken} not valid`); - - return errorResponse(); - } - } - else if (request.auth && request.auth.credentials && 'usageIdentifierKey' in request.auth.credentials) { - const usageIdentifierKey = request.auth.credentials.usageIdentifierKey; - if (usageIdentifierKey !== this.options.apiKey) { - debugLog(`Method ${method} of function ${funName} token ${usageIdentifierKey} not valid`); - - return errorResponse(); - } - } - else { - debugLog(`Missing x-api-key on private function ${funName}`); - - return errorResponse(); - } - } - // Shared mutable state is the root of all evil they say - const requestId = getUniqueId(); - this.requests[requestId] = { done: false }; - this.currentRequestId = requestId; - - const response = h.response(); - const contentType = request.mime || defaultContentType; - - // default request template to '' if we don't have a definition pushed in from serverless or endpoint - const requestTemplate = typeof requestTemplates !== 'undefined' && integration === 'lambda' ? requestTemplates[contentType] : ''; - - // https://hapijs.com/api#route-configuration doesn't seem to support selectively parsing - // so we have to do it ourselves - const contentTypesThatRequirePayloadParsing = ['application/json', 'application/vnd.api+json']; - if (contentTypesThatRequirePayloadParsing.includes(contentType) && request.payload && request.payload.length > 1) { - try { - if (!request.payload || request.payload.length < 1) { - request.payload = '{}'; - } - - request.payload = JSON.parse(request.payload); - } - catch (err) { - debugLog('error in converting request.payload to JSON:', err); - } - } - - debugLog('requestId:', requestId); - debugLog('contentType:', contentType); - debugLog('requestTemplate:', requestTemplate); - debugLog('payload:', request.payload); - - /* HANDLER LAZY LOADING */ - - let userHandler; // The lambda function - Object.assign(process.env, this.originalEnvironment); - - try { - if (this.options.noEnvironment) { - // This evict errors in server when we use aws services like ssm - const baseEnvironment = { - AWS_REGION: 'dev', - }; - if (!process.env.AWS_PROFILE) { - baseEnvironment.AWS_ACCESS_KEY_ID = 'dev'; - baseEnvironment.AWS_SECRET_ACCESS_KEY = 'dev'; - } - - process.env = Object.assign(baseEnvironment, process.env); - } - else { - Object.assign( - process.env, - { AWS_REGION: this.service.provider.region }, - this.service.provider.environment, - this.service.functions[key].environment - ); - } - process.env._HANDLER = fun.handler; - userHandler = functionHelper.createHandler(funOptions, this.options); - } - catch (err) { - return this._reply500(response, `Error while loading ${funName}`, err); - } - - /* REQUEST TEMPLATE PROCESSING (event population) */ - - let event = {}; - - if (integration === 'lambda') { - if (requestTemplate) { - try { - debugLog('_____ REQUEST TEMPLATE PROCESSING _____'); - // Velocity templating language parsing - const velocityContext = createVelocityContext(request, this.velocityContextOptions, request.payload || {}); - event = renderVelocityTemplateObject(requestTemplate, velocityContext); - } - catch (err) { - return this._reply500(response, `Error while parsing template "${contentType}" for ${funName}`, err); - } - } - else if (typeof request.payload === 'object') { - event = request.payload || {}; - } - } - else if (integration === 'lambda-proxy') { - event = createLambdaProxyContext(request, this.options, this.velocityContextOptions.stageVariables); - } - - event.isOffline = true; - - if (this.service.custom && this.service.custom.stageVariables) { - event.stageVariables = this.service.custom.stageVariables; - } - else if (integration !== 'lambda-proxy') { - event.stageVariables = {}; - } - - debugLog('event:', event); - - return new Promise(resolve => { - // We create the context, its callback (context.done/succeed/fail) will send the HTTP response - const lambdaContext = createLambdaContext(fun, this.service.provider, (err, data, fromPromise) => { - // Everything in this block happens once the lambda function has resolved - debugLog('_____ HANDLER RESOLVED _____'); - - // User should not call context.done twice - if (!this.requests[requestId] || this.requests[requestId].done) { - this.printBlankLine(); - const warning = fromPromise - ? `Warning: handler '${funName}' returned a promise and also uses a callback!\nThis is problematic and might cause issues in your lambda.` - : `Warning: context.done called twice within handler '${funName}'!`; - this.serverlessLog(warning); - debugLog('requestId:', requestId); - - return; - } - - this.requests[requestId].done = true; - - let result = data; - let responseName = 'default'; - const { contentHandling, responseContentType } = endpoint; - - /* RESPONSE SELECTION (among endpoint's possible responses) */ - - // Failure handling - let errorStatusCode = 0; - if (err) { - // Since the --useSeparateProcesses option loads the handler in - // a separate process and serverless-offline communicates with it - // over IPC, we are unable to catch JavaScript unhandledException errors - // when the handler code contains bad JavaScript. Instead, we "catch" - // it here and reply in the same way that we would have above when - // we lazy-load the non-IPC handler function. - if (this.options.useSeparateProcesses && err.ipcException) { - return resolve(this._reply500(response, `Error while loading ${funName}`, err)); - } - - const errorMessage = (err.message || err).toString(); - - const re = /\[(\d{3})]/; - const found = errorMessage.match(re); - if (found && found.length > 1) { - errorStatusCode = found[1]; - } - else { - errorStatusCode = '500'; - } - - // Mocks Lambda errors - result = { - errorMessage, - errorType: err.constructor.name, - stackTrace: this._getArrayStackTrace(err.stack), - }; - - this.serverlessLog(`Failure: ${errorMessage}`); - - if (result.stackTrace) { - debugLog(result.stackTrace.join('\n ')); - } - - for (const key in endpoint.responses) { - if (key !== 'default' && errorMessage.match(`^${endpoint.responses[key].selectionPattern || key}$`)) { - responseName = key; - break; - } - } - } - - debugLog(`Using response '${responseName}'`); - const chosenResponse = endpoint.responses[responseName]; - - /* RESPONSE PARAMETERS PROCCESSING */ - - const responseParameters = chosenResponse.responseParameters; - - if (responseParameters) { - - const responseParametersKeys = Object.keys(responseParameters); - - debugLog('_____ RESPONSE PARAMETERS PROCCESSING _____'); - debugLog(`Found ${responseParametersKeys.length} responseParameters for '${responseName}' response`); - - // responseParameters use the following shape: "key": "value" - Object.entries(responseParametersKeys).forEach(([key, value]) => { - - const keyArray = key.split('.'); // eg: "method.response.header.location" - const valueArray = value.split('.'); // eg: "integration.response.body.redirect.url" - - debugLog(`Processing responseParameter "${key}": "${value}"`); - - // For now the plugin only supports modifying headers - if (key.startsWith('method.response.header') && keyArray[3]) { - - const headerName = keyArray.slice(3).join('.'); - let headerValue; - debugLog('Found header in left-hand:', headerName); - - if (value.startsWith('integration.response')) { - if (valueArray[2] === 'body') { - - debugLog('Found body in right-hand'); - headerValue = (valueArray[3] ? jsonPath(result, valueArray.slice(3).join('.')) : result).toString(); - - } - else { - this.printBlankLine(); - this.serverlessLog(`Warning: while processing responseParameter "${key}": "${value}"`); - this.serverlessLog(`Offline plugin only supports "integration.response.body[.JSON_path]" right-hand responseParameter. Found "${value}" instead. Skipping.`); - this.logPluginIssue(); - this.printBlankLine(); - } - } - else { - headerValue = value.match(/^'.*'$/) ? value.slice(1, -1) : value; // See #34 - } - // Applies the header; - debugLog(`Will assign "${headerValue}" to header "${headerName}"`); - response.header(headerName, headerValue); - - } - else { - this.printBlankLine(); - this.serverlessLog(`Warning: while processing responseParameter "${key}": "${value}"`); - this.serverlessLog(`Offline plugin only supports "method.response.header.PARAM_NAME" left-hand responseParameter. Found "${key}" instead. Skipping.`); - this.logPluginIssue(); - this.printBlankLine(); - } - }); - } - - let statusCode = 200; - - if (integration === 'lambda') { - - const endpointResponseHeaders = (endpoint.response && endpoint.response.headers) || {}; - - Object.entries(endpointResponseHeaders) - .filter(([, value]) => typeof value === 'string' && /^'.*?'$/.test(value)) - .forEach(([key, value]) => response.header(key, value.slice(1, -1))); - - /* LAMBDA INTEGRATION RESPONSE TEMPLATE PROCCESSING */ - - // If there is a responseTemplate, we apply it to the result - const { responseTemplates } = chosenResponse; - - if (typeof responseTemplates === 'object') { - const responseTemplatesKeys = Object.keys(responseTemplates); - - if (responseTemplatesKeys.length) { - - // BAD IMPLEMENTATION: first key in responseTemplates - const responseTemplate = responseTemplates[responseContentType]; - - if (responseTemplate && responseTemplate !== '\n') { - - debugLog('_____ RESPONSE TEMPLATE PROCCESSING _____'); - debugLog(`Using responseTemplate '${responseContentType}'`); - - try { - const reponseContext = createVelocityContext(request, this.velocityContextOptions, result); - result = renderVelocityTemplateObject({ root: responseTemplate }, reponseContext).root; - } - catch (error) { - this.serverlessLog(`Error while parsing responseTemplate '${responseContentType}' for lambda ${funName}:`); - console.log(error.stack); - } - } - } - } - - /* LAMBDA INTEGRATION HAPIJS RESPONSE CONFIGURATION */ - - statusCode = errorStatusCode !== 0 ? errorStatusCode : (chosenResponse.statusCode || 200); - - if (!chosenResponse.statusCode) { - this.printBlankLine(); - this.serverlessLog(`Warning: No statusCode found for response "${responseName}".`); - } - - response.header('Content-Type', responseContentType, { - override: false, // Maybe a responseParameter set it already. See #34 - }); - - response.statusCode = statusCode; - - if (contentHandling === 'CONVERT_TO_BINARY') { - response.encoding = 'binary'; - response.source = Buffer.from(result, 'base64'); - response.variety = 'buffer'; - } - else { - if (result && result.body && typeof result.body !== 'string') { - return this._reply500(response, 'According to the API Gateway specs, the body content must be stringified. Check your Lambda response and make sure you are invoking JSON.stringify(YOUR_CONTENT) on your body object', {}); - } - response.source = result; - } - } - else if (integration === 'lambda-proxy') { - - /* LAMBDA PROXY INTEGRATION HAPIJS RESPONSE CONFIGURATION */ - - response.statusCode = statusCode = result.statusCode || 200; - - const headers = {}; - if (result.headers) { - Object.keys(result.headers).forEach(header => { - headers[header] = (headers[header] || []).concat(result.headers[header]); - }); - } - if (result.multiValueHeaders) { - Object.keys(result.multiValueHeaders).forEach(header => { - headers[header] = (headers[header] || []).concat(result.multiValueHeaders[header]); - }); - } - - debugLog('headers', headers); - - Object.keys(headers).forEach(header => { - if (header.toLowerCase() === 'set-cookie') { - headers[header].forEach(headerValue => { - const cookieName = headerValue.slice(0, headerValue.indexOf('=')); - const cookieValue = headerValue.slice(headerValue.indexOf('=') + 1); - h.state(cookieName, cookieValue, { encoding: 'none', strictHeader: false }); - }); - } - else { - headers[header].forEach(headerValue => { - // it looks like Hapi doesn't support multiple headers with the same name, - // appending values is the closest we can come to the AWS behavior. - response.header(header, headerValue, { append: true }); - }); - } - }); - - response.header('Content-Type', 'application/json', { override: false, duplicate: false }); - - if (typeof result.body !== 'undefined') { - if (result.isBase64Encoded) { - response.encoding = 'binary'; - response.source = Buffer.from(result.body, 'base64'); - response.variety = 'buffer'; - } - else { - if (result.body && typeof result.body !== 'string') { - return this._reply500(response, 'According to the API Gateway specs, the body content must be stringified. Check your Lambda response and make sure you are invoking JSON.stringify(YOUR_CONTENT) on your body object', {}); - } - response.source = result.body; - } - } - } - - // Log response - let whatToLog = result; - - try { - whatToLog = JSON.stringify(result); - } - catch (error) { - // nothing - } - finally { - if (this.options.printOutput) this.serverlessLog(err ? `Replying ${statusCode}` : `[${statusCode}] ${whatToLog}`); - debugLog('requestId:', requestId); - } - - // Bon voyage! - resolve(response); - }); - - // Now we are outside of createLambdaContext, so this happens before the handler gets called: - - // We cannot use Hapijs's timeout feature because the logic above can take a significant time, so we implement it ourselves - this.requests[requestId].timeout = this.options.noTimeout ? null : setTimeout( - this._replyTimeout.bind(this, response, resolve, funName, funOptions.funTimeout, requestId), - funOptions.funTimeout - ); - - // If request body validation is enabled, validate body against the request model. - if (requestBodyValidationModel && !this.options.disableModelValidation) { - try { - requestBodyValidator.validate(requestBodyValidationModel, event.body); - } - catch (error) { - // When request body validation fails, APIG will return back 400 as detailed in: - // https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-method-request-validation.html - return resolve(this._replyError(400, response, `Invalid request body for '${funName}' handler`, error)); - } - } - - // Finally we call the handler - debugLog('_____ CALLING HANDLER _____'); - - const cleanup = () => { - this._clearTimeout(requestId); - delete this.requests[requestId]; - }; - - let x; - - if (this.options.showDuration) { - performance.mark(`${requestId}-start`); - - const obs = new PerformanceObserver(list => { - for (const entry of list.getEntries()) { - this.serverlessLog(`Duration ${entry.duration.toFixed(2)} ms (λ: ${entry.name})`); - } - - obs.disconnect(); - }); - - obs.observe({ entryTypes: ['measure'] }); - } - - try { - x = userHandler(event, lambdaContext, (err, result) => { - setTimeout(cleanup, 0); - - if (this.options.showDuration) { - performance.mark(`${requestId}-end`); - performance.measure(funName, `${requestId}-start`, `${requestId}-end`); - } - - return lambdaContext.done(err, result); - }); - - // Promise support - if (!this.requests[requestId].done) { - if (x && typeof x.then === 'function') { - x.then(lambdaContext.succeed).catch(lambdaContext.fail).then(cleanup, cleanup); - } - else if (x instanceof Error) { - lambdaContext.fail(x); - } - } - } - catch (error) { - cleanup(); - - return resolve(this._reply500(response, `Uncaught error in your '${funName}' handler`, error)); - } - }); - }, - }); - }); - }); - } - - _extractAuthFunctionName(endpoint) { - const result = authFunctionNameExtractor(endpoint, this.serverlessLog); - - return result.unsupportedAuth ? null : result.authorizerName; - } - - _configureAuthorization(endpoint, funName, method, epath, servicePath, serviceRuntime) { - if (!endpoint.authorizer) { - return null; - } - - const authFunctionName = this._extractAuthFunctionName(endpoint); - - if (!authFunctionName) { - return null; - } - - this.serverlessLog(`Configuring Authorization: ${endpoint.path} ${authFunctionName}`); - - const authFunction = this.service.getFunction(authFunctionName); - - if (!authFunction) return this.serverlessLog(`WARNING: Authorization function ${authFunctionName} does not exist`); - - const authorizerOptions = { - resultTtlInSeconds: '300', - identitySource: 'method.request.header.Authorization', - identityValidationExpression: '(.*)', - }; - - if (typeof endpoint.authorizer === 'string') { - authorizerOptions.name = authFunctionName; - } - else { - Object.assign(authorizerOptions, endpoint.authorizer); - } - - // Create a unique scheme per endpoint - // This allows the methodArn on the event property to be set appropriately - const authKey = `${funName}-${authFunctionName}-${method}-${epath}`; - const authSchemeName = `scheme-${authKey}`; - const authStrategyName = `strategy-${authKey}`; // set strategy name for the route config - - debugLog(`Creating Authorization scheme for ${authKey}`); - - // Create the Auth Scheme for the endpoint - const scheme = createAuthScheme( - authFunction, - authorizerOptions, - authFunctionName, - epath, - this.options, - this.serverlessLog, - servicePath, - serviceRuntime, - this.serverless - ); - - // Set the auth scheme and strategy on the server - this.server.auth.scheme(authSchemeName, scheme); - this.server.auth.strategy(authStrategyName, authSchemeName); - - return authStrategyName; - } - - // All done, we can listen to incomming requests - async _listen() { - try { - await this.server.start(); - } - catch (e) { - console.error(`Unexpected error while starting serverless-offline server on port ${this.options.port}:`, e); - process.exit(1); - } - - this.printBlankLine(); - this.serverlessLog(`Offline listening on http${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port}`); - - try { - await this.wsServer.start(); - } - catch (e) { - console.error(`Unexpected error while starting serverless-offline websocket server on port ${this.options.wsPort}:`, e); - process.exit(1); - } - - this.printBlankLine(); - this.serverlessLog(`Offline listening on ws${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.wsPort}`); - - this.printBlankLine(); - this.serverlessLog(`Offline listening on http${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.wsPort}/@connections/{connectionId}`); - } - end() { this.serverlessLog('Halting offline server'); functionHelper.cleanup(); - this.server.stop({ timeout: 5000 }) + this.apiGateway.server.stop({ timeout: 5000 }) .then(() => process.exit(this.exitCode)); } - - // Bad news - _replyError(responseCode, response, message, err) { - const stackTrace = this._getArrayStackTrace(err.stack); - - this.serverlessLog(message); - if (stackTrace && stackTrace.length > 0) { - console.log(stackTrace); - } - else { - console.log(err); - } - - response.header('Content-Type', 'application/json'); - - /* eslint-disable no-param-reassign */ - response.statusCode = responseCode; - response.source = { - errorMessage: message, - errorType: err.constructor.name, - stackTrace, - offlineInfo: 'If you believe this is an issue with the plugin please submit it, thanks. https://github.com/dherault/serverless-offline/issues', - }; - /* eslint-enable no-param-reassign */ - this.serverlessLog('Replying error in handler'); - - return response; - } - - _reply500(response, message, err) { - // APIG replies 200 by default on failures - return this._replyError(200, response, message, err); - } - - _replyTimeout(response, resolve, funName, funTimeout, requestId) { - if (this.currentRequestId !== requestId) return; - - this.serverlessLog(`Replying timeout after ${funTimeout}ms`); - /* eslint-disable no-param-reassign */ - response.statusCode = 503; - response.source = `[Serverless-Offline] Your λ handler '${funName}' timed out after ${funTimeout}ms.`; - /* eslint-enable no-param-reassign */ - resolve(response); - } - - _clearTimeout(requestId) { - const { timeout } = this.requests[requestId] || {}; - clearTimeout(timeout); - } - - _createResourceRoutes() { - if (!this.options.resourceRoutes) return true; - const resourceRoutesOptions = this.options.resourceRoutes; - const resourceRoutes = parseResources(this.service.resources); - - if (!resourceRoutes || !Object.keys(resourceRoutes).length) return true; - - this.printBlankLine(); - this.serverlessLog('Routes defined in resources:'); - - Object.entries(resourceRoutes).forEach(([methodId, resourceRoutesObj]) => { - const { isProxy, method, path, pathResource, proxyUri } = resourceRoutesObj; - - if (!isProxy) { - return this.serverlessLog(`WARNING: Only HTTP_PROXY is supported. Path '${pathResource}' is ignored.`); - } - if (!path) { - return this.serverlessLog(`WARNING: Could not resolve path for '${methodId}'.`); - } - - let fullPath = this.options.prefix + (pathResource.startsWith('/') ? pathResource.slice(1) : pathResource); - if (fullPath !== '/' && fullPath.endsWith('/')) fullPath = fullPath.slice(0, -1); - fullPath = fullPath.replace(/\+}/g, '*}'); - - const proxyUriOverwrite = resourceRoutesOptions[methodId] || {}; - const proxyUriInUse = proxyUriOverwrite.Uri || proxyUri; - - if (!proxyUriInUse) { - return this.serverlessLog(`WARNING: Could not load Proxy Uri for '${methodId}'`); - } - - const routeMethod = method === 'ANY' ? '*' : method; - const routeConfig = { cors: this.options.corsConfig }; - - // skip HEAD routes as hapi will fail with 'Method name not allowed: HEAD ...' - // for more details, check https://github.com/dherault/serverless-offline/issues/204 - if (routeMethod === 'HEAD') { - this.serverlessLog('HEAD method event detected. Skipping HAPI server route mapping ...'); - - return; - } - - if (routeMethod !== 'HEAD' && routeMethod !== 'GET') { - routeConfig.payload = { parse: false }; - } - - this.serverlessLog(`${method} ${fullPath} -> ${proxyUriInUse}`); - this.server.route({ - method: routeMethod, - path: fullPath, - config: routeConfig, - handler: (request, h) => { - const { params } = request; - let resultUri = proxyUriInUse; - - Object.entries(params).forEach(([key, value]) => { - resultUri = resultUri.replace(`{${key}}`, value); - }); - - if (request.url.search !== null) { - resultUri += request.url.search; // search is empty string by default - } - - this.serverlessLog(`PROXY ${request.method} ${request.url.path} -> ${resultUri}`); - - return h.proxy({ uri: resultUri, passThrough: true }); - }, - }); - }); - } - - _create404Route() { - // If a {proxy+} route exists, don't conflict with it - if (this.server.match('*', '/{p*}')) return; - - this.server.route({ - method: '*', - path: '/{p*}', - config: { cors: this.options.corsConfig }, - handler: (request, h) => { - const response = h.response({ - statusCode: 404, - error: 'Serverless-offline: route not found.', - currentRoute: `${request.method} - ${request.path}`, - existingRoutes: this.server.table() - .filter(route => route.path !== '/{p*}') // Exclude this (404) route - .sort((a, b) => a.path <= b.path ? -1 : 1) // Sort by path - .map(route => `${route.method} - ${route.path}`), // Human-friendly result - }); - response.statusCode = 404; - - return response; - }, - }); - } - - _getArrayStackTrace(stack) { - if (!stack) return null; - - const splittedStack = stack.split('\n'); - - return splittedStack.slice(0, splittedStack.findIndex(item => item.match(/server.route.handler.createLambdaContext/))).map(line => line.trim()); - } - - _logAndExit() { - // eslint-disable-next-line - console.log.apply(null, arguments); - process.exit(0); - } -} +}; // Serverless exits with code 1 when a promise rejection is unhandled. Not AWS. // Users can still use their own unhandledRejection event though. process.removeAllListeners('unhandledRejection'); - -module.exports = Offline; From 536d1b3fe76808ec93ab4d1bb0b6151374302bd0 Mon Sep 17 00:00:00 2001 From: Daniel Nalborczyk Date: Sat, 15 Jun 2019 21:52:44 -0400 Subject: [PATCH 39/71] Refactor events setup to offline class --- src/ApiGateway.js | 1008 +++++++++++++++++++++------------------------ src/index.js | 98 ++++- 2 files changed, 556 insertions(+), 550 deletions(-) diff --git a/src/ApiGateway.js b/src/ApiGateway.js index 7587c936c..cc7696bb8 100644 --- a/src/ApiGateway.js +++ b/src/ApiGateway.js @@ -182,681 +182,591 @@ module.exports = class ApiGateway { this.serverlessLog(`Offline [http] listening on http${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port}`); } - _createRoutes() { - let serviceRuntime = this.service.provider.runtime; - const defaultContentType = 'application/json'; - const apiKeys = this.service.provider.apiKeys; - const protectedRoutes = []; - - if (!serviceRuntime) { - throw new Error('Missing required property "runtime" for provider.'); + _createRoutes(event, funOptions, protectedRoutes, funName, servicePath, serviceRuntime, defaultContentType, key, fun) { + // Handle Simple http setup, ex. - http: GET users/index + if (typeof event.http === 'string') { + const [method, path] = event.http.split(' '); + event.http = { method, path }; } - if (typeof serviceRuntime !== 'string') { - throw new Error('Provider configuration property "runtime" wasn\'t a string.'); - } + // generate an enpoint via the endpoint class + const endpoint = new Endpoint(event.http, funOptions).generate(); - if (serviceRuntime === 'provided') { - if (this.options.providedRuntime) { - serviceRuntime = this.options.providedRuntime; - } - else { - throw new Error('Runtime "provided" is unsupported. Please add a --providedRuntime CLI option.'); - } - } + const integration = endpoint.integration || 'lambda-proxy'; + const requestBodyValidationModel = (['lambda', 'lambda-proxy'].includes(integration) + ? requestBodyValidator.getModel(this.service.custom, event.http, this.serverlessLog) + : null); + const epath = endpoint.path; + const method = endpoint.method.toUpperCase(); + const requestTemplates = endpoint.requestTemplates; - if (!(serviceRuntime.startsWith('nodejs') || serviceRuntime.startsWith('python') || serviceRuntime.startsWith('ruby'))) { - this.printBlankLine(); - this.serverlessLog(`Warning: found unsupported runtime '${serviceRuntime}'`); + // Prefix must start and end with '/' BUT path must not end with '/' + let fullPath = this.options.prefix + (epath.startsWith('/') ? epath.slice(1) : epath); + if (fullPath !== '/' && fullPath.endsWith('/')) fullPath = fullPath.slice(0, -1); + fullPath = fullPath.replace(/\+}/g, '*}'); - return; + if (event.http.private) { + protectedRoutes.push(`${method}#${fullPath}`); } - // for simple API Key authentication model - if (apiKeys) { - this.serverlessLog(`Key with token: ${this.options.apiKey}`); + this.serverlessLog(`${method} ${fullPath}${requestBodyValidationModel && !this.options.disableModelValidation ? ` - request body will be validated against ${requestBodyValidationModel.name}` : ''}`); - if (this.options.noAuth) { - this.serverlessLog('Authorizers are turned off. You do not need to use x-api-key header.'); - } - else { - this.serverlessLog('Remember to use x-api-key on the request headers'); - } - } + // If the endpoint has an authorization function, create an authStrategy for the route + const authStrategyName = this.options.noAuth ? null : this._configureAuthorization(endpoint, funName, method, epath, servicePath, serviceRuntime); - Object.keys(this.service.functions).forEach(key => { + let cors = null; + if (endpoint.cors) { + cors = { + origin: endpoint.cors.origins || this.options.corsConfig.origin, + headers: endpoint.cors.headers || this.options.corsConfig.headers, + credentials: endpoint.cors.credentials || this.options.corsConfig.credentials, + exposedHeaders: this.options.corsConfig.exposedHeaders, + }; + } - const fun = this.service.getFunction(key); - const funName = key; - const servicePath = path.join(this.serverless.config.servicePath, this.options.location); - const funOptions = functionHelper.getFunctionOptions(fun, key, servicePath, serviceRuntime); + // Route creation + const routeMethod = method === 'ANY' ? '*' : method; - debugLog(`funOptions ${JSON.stringify(funOptions, null, 2)} `); - this.printBlankLine(); - debugLog(funName, 'runtime', serviceRuntime); - this.serverlessLog(`Routes for ${funName}:`); + const state = this.options.disableCookieValidation ? { + parse: false, + failAction: 'ignore', + } : { + parse: true, + failAction: 'error', + }; - if (!fun.events) { - fun.events = []; - } + const routeConfig = { + cors, + auth: authStrategyName, + timeout: { socket: false }, + state, + }; - // Add proxy for lamda invoke - fun.events.push({ - http: { - method: 'POST', - path: `{apiVersion}/functions/${fun.name}/invocations`, - integration: 'lambda', - request: { - template: { - // AWS SDK for NodeJS specifies as 'binary/octet-stream' not 'application/json' - 'binary/octet-stream': '$input.body', - }, - }, - response: { - headers: { - 'Content-Type': 'application/json', - }, - }, - }, - }); + // skip HEAD routes as hapi will fail with 'Method name not allowed: HEAD ...' + // for more details, check https://github.com/dherault/serverless-offline/issues/204 + if (routeMethod === 'HEAD') { + this.serverlessLog('HEAD method event detected. Skipping HAPI server route mapping ...'); - // Adds a route for each http endpoint - // eslint-disable-next-line - fun.events.forEach(event => { + return; + } - if (event.websocket) { - this.apiGatewayWebSocket._createWsAction(fun, funName, servicePath, funOptions, event); + if (routeMethod !== 'HEAD' && routeMethod !== 'GET') { + // maxBytes: Increase request size from 1MB default limit to 10MB. + // Cf AWS API GW payload limits. + routeConfig.payload = { parse: false, maxBytes: 1024 * 1024 * 10 }; + } - return; + this.server.route({ + method: routeMethod, + path: fullPath, + config: routeConfig, + handler: (request, h) => { // Here we go + // Payload processing + const encoding = detectEncoding(request); + + request.payload = request.payload && request.payload.toString(encoding); + request.rawPayload = request.payload; + + // Headers processing + // Hapi lowercases the headers whereas AWS does not + // so we recreate a custom headers object from the raw request + const headersArray = request.raw.req.rawHeaders; + + // During tests, `server.inject` uses *shot*, a package + // for performing injections that does not entirely mimick + // Hapi's usual request object. rawHeaders are then missing + // Hence the fallback for testing + + // Normal usage + if (headersArray) { + request.unprocessedHeaders = {}; + request.multiValueHeaders = {}; + + for (let i = 0; i < headersArray.length; i += 2) { + request.unprocessedHeaders[headersArray[i]] = headersArray[i + 1]; + request.multiValueHeaders[headersArray[i]] = (request.multiValueHeaders[headersArray[i]] || []).concat(headersArray[i + 1]); + } } - if (!event.http) return; - - // Handle Simple http setup, ex. - http: GET users/index - if (typeof event.http === 'string') { - const [method, path] = event.http.split(' '); - event.http = { method, path }; + // Lib testing + else { + request.unprocessedHeaders = request.headers; } - // generate an enpoint via the endpoint class - const endpoint = new Endpoint(event.http, funOptions).generate(); - - const integration = endpoint.integration || 'lambda-proxy'; - const requestBodyValidationModel = (['lambda', 'lambda-proxy'].includes(integration) - ? requestBodyValidator.getModel(this.service.custom, event.http, this.serverlessLog) - : null); - const epath = endpoint.path; - const method = endpoint.method.toUpperCase(); - const requestTemplates = endpoint.requestTemplates; + // Incomming request message + this.printBlankLine(); + this.serverlessLog(`${method} ${request.path} (λ: ${funName})`); - // Prefix must start and end with '/' BUT path must not end with '/' - let fullPath = this.options.prefix + (epath.startsWith('/') ? epath.slice(1) : epath); - if (fullPath !== '/' && fullPath.endsWith('/')) fullPath = fullPath.slice(0, -1); - fullPath = fullPath.replace(/\+}/g, '*}'); + // Check for APIKey + if ((protectedRoutes.includes(`${routeMethod}#${fullPath}`) || protectedRoutes.includes(`ANY#${fullPath}`)) && !this.options.noAuth) { + const errorResponse = () => h.response({ message: 'Forbidden' }).code(403).type('application/json').header('x-amzn-ErrorType', 'ForbiddenException'); - if (event.http.private) { - protectedRoutes.push(`${method}#${fullPath}`); - } + if ('x-api-key' in request.headers) { + const requestToken = request.headers['x-api-key']; + if (requestToken !== this.options.apiKey) { + debugLog(`Method ${method} of function ${funName} token ${requestToken} not valid`); - this.serverlessLog(`${method} ${fullPath}${requestBodyValidationModel && !this.options.disableModelValidation ? ` - request body will be validated against ${requestBodyValidationModel.name}` : ''}`); + return errorResponse(); + } + } + else if (request.auth && request.auth.credentials && 'usageIdentifierKey' in request.auth.credentials) { + const usageIdentifierKey = request.auth.credentials.usageIdentifierKey; + if (usageIdentifierKey !== this.options.apiKey) { + debugLog(`Method ${method} of function ${funName} token ${usageIdentifierKey} not valid`); - // If the endpoint has an authorization function, create an authStrategy for the route - const authStrategyName = this.options.noAuth ? null : this._configureAuthorization(endpoint, funName, method, epath, servicePath, serviceRuntime); + return errorResponse(); + } + } + else { + debugLog(`Missing x-api-key on private function ${funName}`); - let cors = null; - if (endpoint.cors) { - cors = { - origin: endpoint.cors.origins || this.options.corsConfig.origin, - headers: endpoint.cors.headers || this.options.corsConfig.headers, - credentials: endpoint.cors.credentials || this.options.corsConfig.credentials, - exposedHeaders: this.options.corsConfig.exposedHeaders, - }; + return errorResponse(); + } } + // Shared mutable state is the root of all evil they say + const requestId = getUniqueId(); + this.requests[requestId] = { done: false }; + this.currentRequestId = requestId; + + const response = h.response(); + const contentType = request.mime || defaultContentType; + + // default request template to '' if we don't have a definition pushed in from serverless or endpoint + const requestTemplate = typeof requestTemplates !== 'undefined' && integration === 'lambda' ? requestTemplates[contentType] : ''; + + // https://hapijs.com/api#route-configuration doesn't seem to support selectively parsing + // so we have to do it ourselves + const contentTypesThatRequirePayloadParsing = ['application/json', 'application/vnd.api+json']; + if (contentTypesThatRequirePayloadParsing.includes(contentType) && request.payload && request.payload.length > 1) { + try { + if (!request.payload || request.payload.length < 1) { + request.payload = '{}'; + } - // Route creation - const routeMethod = method === 'ANY' ? '*' : method; - - const state = this.options.disableCookieValidation ? { - parse: false, - failAction: 'ignore', - } : { - parse: true, - failAction: 'error', - }; - - const routeConfig = { - cors, - auth: authStrategyName, - timeout: { socket: false }, - state, - }; - - // skip HEAD routes as hapi will fail with 'Method name not allowed: HEAD ...' - // for more details, check https://github.com/dherault/serverless-offline/issues/204 - if (routeMethod === 'HEAD') { - this.serverlessLog('HEAD method event detected. Skipping HAPI server route mapping ...'); - - return; + request.payload = JSON.parse(request.payload); + } + catch (err) { + debugLog('error in converting request.payload to JSON:', err); + } } - if (routeMethod !== 'HEAD' && routeMethod !== 'GET') { - // maxBytes: Increase request size from 1MB default limit to 10MB. - // Cf AWS API GW payload limits. - routeConfig.payload = { parse: false, maxBytes: 1024 * 1024 * 10 }; + debugLog('requestId:', requestId); + debugLog('contentType:', contentType); + debugLog('requestTemplate:', requestTemplate); + debugLog('payload:', request.payload); + + /* HANDLER LAZY LOADING */ + + let userHandler; // The lambda function + Object.assign(process.env, this.originalEnvironment); + + try { + if (this.options.noEnvironment) { + // This evict errors in server when we use aws services like ssm + const baseEnvironment = { + AWS_REGION: 'dev', + }; + if (!process.env.AWS_PROFILE) { + baseEnvironment.AWS_ACCESS_KEY_ID = 'dev'; + baseEnvironment.AWS_SECRET_ACCESS_KEY = 'dev'; + } + + process.env = Object.assign(baseEnvironment, process.env); + } + else { + Object.assign( + process.env, + { AWS_REGION: this.service.provider.region }, + this.service.provider.environment, + this.service.functions[key].environment + ); + } + process.env._HANDLER = fun.handler; + userHandler = functionHelper.createHandler(funOptions, this.options); + } + catch (err) { + return this._reply500(response, `Error while loading ${funName}`, err); } - this.server.route({ - method: routeMethod, - path: fullPath, - config: routeConfig, - handler: (request, h) => { // Here we go - // Payload processing - const encoding = detectEncoding(request); - - request.payload = request.payload && request.payload.toString(encoding); - request.rawPayload = request.payload; - - // Headers processing - // Hapi lowercases the headers whereas AWS does not - // so we recreate a custom headers object from the raw request - const headersArray = request.raw.req.rawHeaders; - - // During tests, `server.inject` uses *shot*, a package - // for performing injections that does not entirely mimick - // Hapi's usual request object. rawHeaders are then missing - // Hence the fallback for testing - - // Normal usage - if (headersArray) { - request.unprocessedHeaders = {}; - request.multiValueHeaders = {}; - - for (let i = 0; i < headersArray.length; i += 2) { - request.unprocessedHeaders[headersArray[i]] = headersArray[i + 1]; - request.multiValueHeaders[headersArray[i]] = (request.multiValueHeaders[headersArray[i]] || []).concat(headersArray[i + 1]); - } + /* REQUEST TEMPLATE PROCESSING (event population) */ + + let event = {}; + + if (integration === 'lambda') { + if (requestTemplate) { + try { + debugLog('_____ REQUEST TEMPLATE PROCESSING _____'); + // Velocity templating language parsing + const velocityContext = createVelocityContext(request, this.velocityContextOptions, request.payload || {}); + event = renderVelocityTemplateObject(requestTemplate, velocityContext); } - // Lib testing - else { - request.unprocessedHeaders = request.headers; + catch (err) { + return this._reply500(response, `Error while parsing template "${contentType}" for ${funName}`, err); } + } + else if (typeof request.payload === 'object') { + event = request.payload || {}; + } + } + else if (integration === 'lambda-proxy') { + event = createLambdaProxyContext(request, this.options, this.velocityContextOptions.stageVariables); + } - // Incomming request message - this.printBlankLine(); - this.serverlessLog(`${method} ${request.path} (λ: ${funName})`); + event.isOffline = true; - // Check for APIKey - if ((protectedRoutes.includes(`${routeMethod}#${fullPath}`) || protectedRoutes.includes(`ANY#${fullPath}`)) && !this.options.noAuth) { - const errorResponse = () => h.response({ message: 'Forbidden' }).code(403).type('application/json').header('x-amzn-ErrorType', 'ForbiddenException'); + if (this.service.custom && this.service.custom.stageVariables) { + event.stageVariables = this.service.custom.stageVariables; + } + else if (integration !== 'lambda-proxy') { + event.stageVariables = {}; + } - if ('x-api-key' in request.headers) { - const requestToken = request.headers['x-api-key']; - if (requestToken !== this.options.apiKey) { - debugLog(`Method ${method} of function ${funName} token ${requestToken} not valid`); + debugLog('event:', event); - return errorResponse(); - } - } - else if (request.auth && request.auth.credentials && 'usageIdentifierKey' in request.auth.credentials) { - const usageIdentifierKey = request.auth.credentials.usageIdentifierKey; - if (usageIdentifierKey !== this.options.apiKey) { - debugLog(`Method ${method} of function ${funName} token ${usageIdentifierKey} not valid`); + return new Promise(resolve => { + // We create the context, its callback (context.done/succeed/fail) will send the HTTP response + const lambdaContext = createLambdaContext(fun, this.service.provider, (err, data, fromPromise) => { + // Everything in this block happens once the lambda function has resolved + debugLog('_____ HANDLER RESOLVED _____'); - return errorResponse(); - } - } - else { - debugLog(`Missing x-api-key on private function ${funName}`); + // User should not call context.done twice + if (!this.requests[requestId] || this.requests[requestId].done) { + this.printBlankLine(); + const warning = fromPromise + ? `Warning: handler '${funName}' returned a promise and also uses a callback!\nThis is problematic and might cause issues in your lambda.` + : `Warning: context.done called twice within handler '${funName}'!`; + this.serverlessLog(warning); + debugLog('requestId:', requestId); - return errorResponse(); - } + return; } - // Shared mutable state is the root of all evil they say - const requestId = getUniqueId(); - this.requests[requestId] = { done: false }; - this.currentRequestId = requestId; - - const response = h.response(); - const contentType = request.mime || defaultContentType; - - // default request template to '' if we don't have a definition pushed in from serverless or endpoint - const requestTemplate = typeof requestTemplates !== 'undefined' && integration === 'lambda' ? requestTemplates[contentType] : ''; - - // https://hapijs.com/api#route-configuration doesn't seem to support selectively parsing - // so we have to do it ourselves - const contentTypesThatRequirePayloadParsing = ['application/json', 'application/vnd.api+json']; - if (contentTypesThatRequirePayloadParsing.includes(contentType) && request.payload && request.payload.length > 1) { - try { - if (!request.payload || request.payload.length < 1) { - request.payload = '{}'; - } - request.payload = JSON.parse(request.payload); + this.requests[requestId].done = true; + + let result = data; + let responseName = 'default'; + const { contentHandling, responseContentType } = endpoint; + + /* RESPONSE SELECTION (among endpoint's possible responses) */ + + // Failure handling + let errorStatusCode = 0; + if (err) { + // Since the --useSeparateProcesses option loads the handler in + // a separate process and serverless-offline communicates with it + // over IPC, we are unable to catch JavaScript unhandledException errors + // when the handler code contains bad JavaScript. Instead, we "catch" + // it here and reply in the same way that we would have above when + // we lazy-load the non-IPC handler function. + if (this.options.useSeparateProcesses && err.ipcException) { + return resolve(this._reply500(response, `Error while loading ${funName}`, err)); } - catch (err) { - debugLog('error in converting request.payload to JSON:', err); - } - } - - debugLog('requestId:', requestId); - debugLog('contentType:', contentType); - debugLog('requestTemplate:', requestTemplate); - debugLog('payload:', request.payload); - /* HANDLER LAZY LOADING */ + const errorMessage = (err.message || err).toString(); - let userHandler; // The lambda function - Object.assign(process.env, this.originalEnvironment); - - try { - if (this.options.noEnvironment) { - // This evict errors in server when we use aws services like ssm - const baseEnvironment = { - AWS_REGION: 'dev', - }; - if (!process.env.AWS_PROFILE) { - baseEnvironment.AWS_ACCESS_KEY_ID = 'dev'; - baseEnvironment.AWS_SECRET_ACCESS_KEY = 'dev'; - } - - process.env = Object.assign(baseEnvironment, process.env); + const re = /\[(\d{3})]/; + const found = errorMessage.match(re); + if (found && found.length > 1) { + errorStatusCode = found[1]; } else { - Object.assign( - process.env, - { AWS_REGION: this.service.provider.region }, - this.service.provider.environment, - this.service.functions[key].environment - ); + errorStatusCode = '500'; } - process.env._HANDLER = fun.handler; - userHandler = functionHelper.createHandler(funOptions, this.options); - } - catch (err) { - return this._reply500(response, `Error while loading ${funName}`, err); - } - /* REQUEST TEMPLATE PROCESSING (event population) */ + // Mocks Lambda errors + result = { + errorMessage, + errorType: err.constructor.name, + stackTrace: this._getArrayStackTrace(err.stack), + }; - let event = {}; + this.serverlessLog(`Failure: ${errorMessage}`); - if (integration === 'lambda') { - if (requestTemplate) { - try { - debugLog('_____ REQUEST TEMPLATE PROCESSING _____'); - // Velocity templating language parsing - const velocityContext = createVelocityContext(request, this.velocityContextOptions, request.payload || {}); - event = renderVelocityTemplateObject(requestTemplate, velocityContext); - } - catch (err) { - return this._reply500(response, `Error while parsing template "${contentType}" for ${funName}`, err); - } + if (result.stackTrace) { + debugLog(result.stackTrace.join('\n ')); } - else if (typeof request.payload === 'object') { - event = request.payload || {}; - } - } - else if (integration === 'lambda-proxy') { - event = createLambdaProxyContext(request, this.options, this.velocityContextOptions.stageVariables); - } - - event.isOffline = true; - - if (this.service.custom && this.service.custom.stageVariables) { - event.stageVariables = this.service.custom.stageVariables; - } - else if (integration !== 'lambda-proxy') { - event.stageVariables = {}; - } - - debugLog('event:', event); - - return new Promise(resolve => { - // We create the context, its callback (context.done/succeed/fail) will send the HTTP response - const lambdaContext = createLambdaContext(fun, this.service.provider, (err, data, fromPromise) => { - // Everything in this block happens once the lambda function has resolved - debugLog('_____ HANDLER RESOLVED _____'); - - // User should not call context.done twice - if (!this.requests[requestId] || this.requests[requestId].done) { - this.printBlankLine(); - const warning = fromPromise - ? `Warning: handler '${funName}' returned a promise and also uses a callback!\nThis is problematic and might cause issues in your lambda.` - : `Warning: context.done called twice within handler '${funName}'!`; - this.serverlessLog(warning); - debugLog('requestId:', requestId); - return; + for (const key in endpoint.responses) { + if (key !== 'default' && errorMessage.match(`^${endpoint.responses[key].selectionPattern || key}$`)) { + responseName = key; + break; } + } + } - this.requests[requestId].done = true; - - let result = data; - let responseName = 'default'; - const { contentHandling, responseContentType } = endpoint; - - /* RESPONSE SELECTION (among endpoint's possible responses) */ - - // Failure handling - let errorStatusCode = 0; - if (err) { - // Since the --useSeparateProcesses option loads the handler in - // a separate process and serverless-offline communicates with it - // over IPC, we are unable to catch JavaScript unhandledException errors - // when the handler code contains bad JavaScript. Instead, we "catch" - // it here and reply in the same way that we would have above when - // we lazy-load the non-IPC handler function. - if (this.options.useSeparateProcesses && err.ipcException) { - return resolve(this._reply500(response, `Error while loading ${funName}`, err)); - } - - const errorMessage = (err.message || err).toString(); - - const re = /\[(\d{3})]/; - const found = errorMessage.match(re); - if (found && found.length > 1) { - errorStatusCode = found[1]; - } - else { - errorStatusCode = '500'; - } - - // Mocks Lambda errors - result = { - errorMessage, - errorType: err.constructor.name, - stackTrace: this._getArrayStackTrace(err.stack), - }; - - this.serverlessLog(`Failure: ${errorMessage}`); - - if (result.stackTrace) { - debugLog(result.stackTrace.join('\n ')); - } - - for (const key in endpoint.responses) { - if (key !== 'default' && errorMessage.match(`^${endpoint.responses[key].selectionPattern || key}$`)) { - responseName = key; - break; - } - } - } - - debugLog(`Using response '${responseName}'`); - const chosenResponse = endpoint.responses[responseName]; - - /* RESPONSE PARAMETERS PROCCESSING */ + debugLog(`Using response '${responseName}'`); + const chosenResponse = endpoint.responses[responseName]; - const responseParameters = chosenResponse.responseParameters; + /* RESPONSE PARAMETERS PROCCESSING */ - if (responseParameters) { + const responseParameters = chosenResponse.responseParameters; - const responseParametersKeys = Object.keys(responseParameters); + if (responseParameters) { - debugLog('_____ RESPONSE PARAMETERS PROCCESSING _____'); - debugLog(`Found ${responseParametersKeys.length} responseParameters for '${responseName}' response`); + const responseParametersKeys = Object.keys(responseParameters); - // responseParameters use the following shape: "key": "value" - Object.entries(responseParametersKeys).forEach(([key, value]) => { + debugLog('_____ RESPONSE PARAMETERS PROCCESSING _____'); + debugLog(`Found ${responseParametersKeys.length} responseParameters for '${responseName}' response`); - const keyArray = key.split('.'); // eg: "method.response.header.location" - const valueArray = value.split('.'); // eg: "integration.response.body.redirect.url" + // responseParameters use the following shape: "key": "value" + Object.entries(responseParametersKeys).forEach(([key, value]) => { - debugLog(`Processing responseParameter "${key}": "${value}"`); + const keyArray = key.split('.'); // eg: "method.response.header.location" + const valueArray = value.split('.'); // eg: "integration.response.body.redirect.url" - // For now the plugin only supports modifying headers - if (key.startsWith('method.response.header') && keyArray[3]) { + debugLog(`Processing responseParameter "${key}": "${value}"`); - const headerName = keyArray.slice(3).join('.'); - let headerValue; - debugLog('Found header in left-hand:', headerName); + // For now the plugin only supports modifying headers + if (key.startsWith('method.response.header') && keyArray[3]) { - if (value.startsWith('integration.response')) { - if (valueArray[2] === 'body') { + const headerName = keyArray.slice(3).join('.'); + let headerValue; + debugLog('Found header in left-hand:', headerName); - debugLog('Found body in right-hand'); - headerValue = (valueArray[3] ? jsonPath(result, valueArray.slice(3).join('.')) : result).toString(); + if (value.startsWith('integration.response')) { + if (valueArray[2] === 'body') { - } - else { - this.printBlankLine(); - this.serverlessLog(`Warning: while processing responseParameter "${key}": "${value}"`); - this.serverlessLog(`Offline plugin only supports "integration.response.body[.JSON_path]" right-hand responseParameter. Found "${value}" instead. Skipping.`); - this.logPluginIssue(); - this.printBlankLine(); - } - } - else { - headerValue = value.match(/^'.*'$/) ? value.slice(1, -1) : value; // See #34 - } - // Applies the header; - debugLog(`Will assign "${headerValue}" to header "${headerName}"`); - response.header(headerName, headerValue); + debugLog('Found body in right-hand'); + headerValue = (valueArray[3] ? jsonPath(result, valueArray.slice(3).join('.')) : result).toString(); } else { this.printBlankLine(); this.serverlessLog(`Warning: while processing responseParameter "${key}": "${value}"`); - this.serverlessLog(`Offline plugin only supports "method.response.header.PARAM_NAME" left-hand responseParameter. Found "${key}" instead. Skipping.`); + this.serverlessLog(`Offline plugin only supports "integration.response.body[.JSON_path]" right-hand responseParameter. Found "${value}" instead. Skipping.`); this.logPluginIssue(); this.printBlankLine(); } - }); + } + else { + headerValue = value.match(/^'.*'$/) ? value.slice(1, -1) : value; // See #34 + } + // Applies the header; + debugLog(`Will assign "${headerValue}" to header "${headerName}"`); + response.header(headerName, headerValue); + } + else { + this.printBlankLine(); + this.serverlessLog(`Warning: while processing responseParameter "${key}": "${value}"`); + this.serverlessLog(`Offline plugin only supports "method.response.header.PARAM_NAME" left-hand responseParameter. Found "${key}" instead. Skipping.`); + this.logPluginIssue(); + this.printBlankLine(); + } + }); + } - let statusCode = 200; + let statusCode = 200; - if (integration === 'lambda') { + if (integration === 'lambda') { - const endpointResponseHeaders = (endpoint.response && endpoint.response.headers) || {}; + const endpointResponseHeaders = (endpoint.response && endpoint.response.headers) || {}; - Object.entries(endpointResponseHeaders) - .filter(([, value]) => typeof value === 'string' && /^'.*?'$/.test(value)) - .forEach(([key, value]) => response.header(key, value.slice(1, -1))); + Object.entries(endpointResponseHeaders) + .filter(([, value]) => typeof value === 'string' && /^'.*?'$/.test(value)) + .forEach(([key, value]) => response.header(key, value.slice(1, -1))); - /* LAMBDA INTEGRATION RESPONSE TEMPLATE PROCCESSING */ + /* LAMBDA INTEGRATION RESPONSE TEMPLATE PROCCESSING */ - // If there is a responseTemplate, we apply it to the result - const { responseTemplates } = chosenResponse; + // If there is a responseTemplate, we apply it to the result + const { responseTemplates } = chosenResponse; - if (typeof responseTemplates === 'object') { - const responseTemplatesKeys = Object.keys(responseTemplates); + if (typeof responseTemplates === 'object') { + const responseTemplatesKeys = Object.keys(responseTemplates); - if (responseTemplatesKeys.length) { + if (responseTemplatesKeys.length) { - // BAD IMPLEMENTATION: first key in responseTemplates - const responseTemplate = responseTemplates[responseContentType]; + // BAD IMPLEMENTATION: first key in responseTemplates + const responseTemplate = responseTemplates[responseContentType]; - if (responseTemplate && responseTemplate !== '\n') { + if (responseTemplate && responseTemplate !== '\n') { - debugLog('_____ RESPONSE TEMPLATE PROCCESSING _____'); - debugLog(`Using responseTemplate '${responseContentType}'`); + debugLog('_____ RESPONSE TEMPLATE PROCCESSING _____'); + debugLog(`Using responseTemplate '${responseContentType}'`); - try { - const reponseContext = createVelocityContext(request, this.velocityContextOptions, result); - result = renderVelocityTemplateObject({ root: responseTemplate }, reponseContext).root; - } - catch (error) { - this.serverlessLog(`Error while parsing responseTemplate '${responseContentType}' for lambda ${funName}:`); - console.log(error.stack); - } - } + try { + const reponseContext = createVelocityContext(request, this.velocityContextOptions, result); + result = renderVelocityTemplateObject({ root: responseTemplate }, reponseContext).root; + } + catch (error) { + this.serverlessLog(`Error while parsing responseTemplate '${responseContentType}' for lambda ${funName}:`); + console.log(error.stack); } } + } + } - /* LAMBDA INTEGRATION HAPIJS RESPONSE CONFIGURATION */ + /* LAMBDA INTEGRATION HAPIJS RESPONSE CONFIGURATION */ - statusCode = errorStatusCode !== 0 ? errorStatusCode : (chosenResponse.statusCode || 200); + statusCode = errorStatusCode !== 0 ? errorStatusCode : (chosenResponse.statusCode || 200); - if (!chosenResponse.statusCode) { - this.printBlankLine(); - this.serverlessLog(`Warning: No statusCode found for response "${responseName}".`); - } + if (!chosenResponse.statusCode) { + this.printBlankLine(); + this.serverlessLog(`Warning: No statusCode found for response "${responseName}".`); + } - response.header('Content-Type', responseContentType, { - override: false, // Maybe a responseParameter set it already. See #34 - }); + response.header('Content-Type', responseContentType, { + override: false, // Maybe a responseParameter set it already. See #34 + }); - response.statusCode = statusCode; + response.statusCode = statusCode; - if (contentHandling === 'CONVERT_TO_BINARY') { - response.encoding = 'binary'; - response.source = Buffer.from(result, 'base64'); - response.variety = 'buffer'; - } - else { - if (result && result.body && typeof result.body !== 'string') { - return this._reply500(response, 'According to the API Gateway specs, the body content must be stringified. Check your Lambda response and make sure you are invoking JSON.stringify(YOUR_CONTENT) on your body object', {}); - } - response.source = result; - } + if (contentHandling === 'CONVERT_TO_BINARY') { + response.encoding = 'binary'; + response.source = Buffer.from(result, 'base64'); + response.variety = 'buffer'; + } + else { + if (result && result.body && typeof result.body !== 'string') { + return this._reply500(response, 'According to the API Gateway specs, the body content must be stringified. Check your Lambda response and make sure you are invoking JSON.stringify(YOUR_CONTENT) on your body object', {}); } - else if (integration === 'lambda-proxy') { + response.source = result; + } + } + else if (integration === 'lambda-proxy') { - /* LAMBDA PROXY INTEGRATION HAPIJS RESPONSE CONFIGURATION */ + /* LAMBDA PROXY INTEGRATION HAPIJS RESPONSE CONFIGURATION */ - response.statusCode = statusCode = result.statusCode || 200; + response.statusCode = statusCode = result.statusCode || 200; - const headers = {}; - if (result.headers) { - Object.keys(result.headers).forEach(header => { - headers[header] = (headers[header] || []).concat(result.headers[header]); - }); - } - if (result.multiValueHeaders) { - Object.keys(result.multiValueHeaders).forEach(header => { - headers[header] = (headers[header] || []).concat(result.multiValueHeaders[header]); - }); - } + const headers = {}; + if (result.headers) { + Object.keys(result.headers).forEach(header => { + headers[header] = (headers[header] || []).concat(result.headers[header]); + }); + } + if (result.multiValueHeaders) { + Object.keys(result.multiValueHeaders).forEach(header => { + headers[header] = (headers[header] || []).concat(result.multiValueHeaders[header]); + }); + } - debugLog('headers', headers); + debugLog('headers', headers); - Object.keys(headers).forEach(header => { - if (header.toLowerCase() === 'set-cookie') { - headers[header].forEach(headerValue => { - const cookieName = headerValue.slice(0, headerValue.indexOf('=')); - const cookieValue = headerValue.slice(headerValue.indexOf('=') + 1); - h.state(cookieName, cookieValue, { encoding: 'none', strictHeader: false }); - }); - } - else { - headers[header].forEach(headerValue => { - // it looks like Hapi doesn't support multiple headers with the same name, - // appending values is the closest we can come to the AWS behavior. - response.header(header, headerValue, { append: true }); - }); - } + Object.keys(headers).forEach(header => { + if (header.toLowerCase() === 'set-cookie') { + headers[header].forEach(headerValue => { + const cookieName = headerValue.slice(0, headerValue.indexOf('=')); + const cookieValue = headerValue.slice(headerValue.indexOf('=') + 1); + h.state(cookieName, cookieValue, { encoding: 'none', strictHeader: false }); }); - - response.header('Content-Type', 'application/json', { override: false, duplicate: false }); - - if (typeof result.body !== 'undefined') { - if (result.isBase64Encoded) { - response.encoding = 'binary'; - response.source = Buffer.from(result.body, 'base64'); - response.variety = 'buffer'; - } - else { - if (result.body && typeof result.body !== 'string') { - return this._reply500(response, 'According to the API Gateway specs, the body content must be stringified. Check your Lambda response and make sure you are invoking JSON.stringify(YOUR_CONTENT) on your body object', {}); - } - response.source = result.body; - } - } } + else { + headers[header].forEach(headerValue => { + // it looks like Hapi doesn't support multiple headers with the same name, + // appending values is the closest we can come to the AWS behavior. + response.header(header, headerValue, { append: true }); + }); + } + }); - // Log response - let whatToLog = result; + response.header('Content-Type', 'application/json', { override: false, duplicate: false }); - try { - whatToLog = JSON.stringify(result); + if (typeof result.body !== 'undefined') { + if (result.isBase64Encoded) { + response.encoding = 'binary'; + response.source = Buffer.from(result.body, 'base64'); + response.variety = 'buffer'; } - catch (error) { - // nothing - } - finally { - if (this.options.printOutput) this.serverlessLog(err ? `Replying ${statusCode}` : `[${statusCode}] ${whatToLog}`); - debugLog('requestId:', requestId); + else { + if (result.body && typeof result.body !== 'string') { + return this._reply500(response, 'According to the API Gateway specs, the body content must be stringified. Check your Lambda response and make sure you are invoking JSON.stringify(YOUR_CONTENT) on your body object', {}); + } + response.source = result.body; } + } + } - // Bon voyage! - resolve(response); - }); + // Log response + let whatToLog = result; - // Now we are outside of createLambdaContext, so this happens before the handler gets called: + try { + whatToLog = JSON.stringify(result); + } + catch (error) { + // nothing + } + finally { + if (this.options.printOutput) this.serverlessLog(err ? `Replying ${statusCode}` : `[${statusCode}] ${whatToLog}`); + debugLog('requestId:', requestId); + } - // We cannot use Hapijs's timeout feature because the logic above can take a significant time, so we implement it ourselves - this.requests[requestId].timeout = this.options.noTimeout ? null : setTimeout( - this._replyTimeout.bind(this, response, resolve, funName, funOptions.funTimeout, requestId), - funOptions.funTimeout - ); + // Bon voyage! + resolve(response); + }); - // If request body validation is enabled, validate body against the request model. - if (requestBodyValidationModel && !this.options.disableModelValidation) { - try { - requestBodyValidator.validate(requestBodyValidationModel, event.body); - } - catch (error) { - // When request body validation fails, APIG will return back 400 as detailed in: - // https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-method-request-validation.html - return resolve(this._replyError(400, response, `Invalid request body for '${funName}' handler`, error)); - } - } + // Now we are outside of createLambdaContext, so this happens before the handler gets called: - // Finally we call the handler - debugLog('_____ CALLING HANDLER _____'); + // We cannot use Hapijs's timeout feature because the logic above can take a significant time, so we implement it ourselves + this.requests[requestId].timeout = this.options.noTimeout ? null : setTimeout( + this._replyTimeout.bind(this, response, resolve, funName, funOptions.funTimeout, requestId), + funOptions.funTimeout + ); - const cleanup = () => { - this._clearTimeout(requestId); - delete this.requests[requestId]; - }; + // If request body validation is enabled, validate body against the request model. + if (requestBodyValidationModel && !this.options.disableModelValidation) { + try { + requestBodyValidator.validate(requestBodyValidationModel, event.body); + } + catch (error) { + // When request body validation fails, APIG will return back 400 as detailed in: + // https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-method-request-validation.html + return resolve(this._replyError(400, response, `Invalid request body for '${funName}' handler`, error)); + } + } - let x; + // Finally we call the handler + debugLog('_____ CALLING HANDLER _____'); - if (this.options.showDuration) { - performance.mark(`${requestId}-start`); + const cleanup = () => { + this._clearTimeout(requestId); + delete this.requests[requestId]; + }; - const obs = new PerformanceObserver(list => { - for (const entry of list.getEntries()) { - this.serverlessLog(`Duration ${entry.duration.toFixed(2)} ms (λ: ${entry.name})`); - } + let x; - obs.disconnect(); - }); + if (this.options.showDuration) { + performance.mark(`${requestId}-start`); - obs.observe({ entryTypes: ['measure'] }); + const obs = new PerformanceObserver(list => { + for (const entry of list.getEntries()) { + this.serverlessLog(`Duration ${entry.duration.toFixed(2)} ms (λ: ${entry.name})`); } - try { - x = userHandler(event, lambdaContext, (err, result) => { - setTimeout(cleanup, 0); + obs.disconnect(); + }); - if (this.options.showDuration) { - performance.mark(`${requestId}-end`); - performance.measure(funName, `${requestId}-start`, `${requestId}-end`); - } + obs.observe({ entryTypes: ['measure'] }); + } - return lambdaContext.done(err, result); - }); + try { + x = userHandler(event, lambdaContext, (err, result) => { + setTimeout(cleanup, 0); - // Promise support - if (!this.requests[requestId].done) { - if (x && typeof x.then === 'function') { - x.then(lambdaContext.succeed).catch(lambdaContext.fail).then(cleanup, cleanup); - } - else if (x instanceof Error) { - lambdaContext.fail(x); - } - } + if (this.options.showDuration) { + performance.mark(`${requestId}-end`); + performance.measure(funName, `${requestId}-start`, `${requestId}-end`); } - catch (error) { - cleanup(); - return resolve(this._reply500(response, `Uncaught error in your '${funName}' handler`, error)); - } + return lambdaContext.done(err, result); }); - }, + + // Promise support + if (!this.requests[requestId].done) { + if (x && typeof x.then === 'function') { + x.then(lambdaContext.succeed).catch(lambdaContext.fail).then(cleanup, cleanup); + } + else if (x instanceof Error) { + lambdaContext.fail(x); + } + } + } + catch (error) { + cleanup(); + + return resolve(this._reply500(response, `Uncaught error in your '${funName}' handler`, error)); + } }); - }); + }, }); } diff --git a/src/index.js b/src/index.js index 190b88e75..680f7febe 100755 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ 'use strict'; +const path = require('path'); const { exec } = require('child_process'); const ApiGateway = require('./ApiGateway'); const ApiGatewayWebSocket = require('./ApiGatewayWebSocket'); @@ -225,7 +226,7 @@ module.exports = class Offline { const server = this.apiGateway._createServer(); this.apiGatewayWebSocket._createWebSocket(); - this.apiGateway._createRoutes(); // API Gateway emulation + this._setupEvents(); this.apiGateway._createResourceRoutes(); // HTTP Proxy defined in Resource this.apiGateway._create404Route(); // Not found handling @@ -312,6 +313,101 @@ module.exports = class Offline { this.apiGateway.server.stop({ timeout: 5000 }) .then(() => process.exit(this.exitCode)); } + + _setupEvents() { + let serviceRuntime = this.service.provider.runtime; + const defaultContentType = 'application/json'; + const apiKeys = this.service.provider.apiKeys; + const protectedRoutes = []; + + if (!serviceRuntime) { + throw new Error('Missing required property "runtime" for provider.'); + } + + if (typeof serviceRuntime !== 'string') { + throw new Error('Provider configuration property "runtime" wasn\'t a string.'); + } + + if (serviceRuntime === 'provided') { + if (this.options.providedRuntime) { + serviceRuntime = this.options.providedRuntime; + } + else { + throw new Error('Runtime "provided" is unsupported. Please add a --providedRuntime CLI option.'); + } + } + + if (!(serviceRuntime.startsWith('nodejs') || serviceRuntime.startsWith('python') || serviceRuntime.startsWith('ruby'))) { + this.printBlankLine(); + this.serverlessLog(`Warning: found unsupported runtime '${serviceRuntime}'`); + + return; + } + + // for simple API Key authentication model + if (apiKeys) { + this.serverlessLog(`Key with token: ${this.options.apiKey}`); + + if (this.options.noAuth) { + this.serverlessLog('Authorizers are turned off. You do not need to use x-api-key header.'); + } + else { + this.serverlessLog('Remember to use x-api-key on the request headers'); + } + } + + Object.keys(this.service.functions).forEach(key => { + + const fun = this.service.getFunction(key); + const funName = key; + const servicePath = path.join(this.serverless.config.servicePath, this.options.location); + const funOptions = functionHelper.getFunctionOptions(fun, key, servicePath, serviceRuntime); + + debugLog(`funOptions ${JSON.stringify(funOptions, null, 2)} `); + this.printBlankLine(); + debugLog(funName, 'runtime', serviceRuntime); + this.serverlessLog(`Routes for ${funName}:`); + + if (!fun.events) { + fun.events = []; + } + + // Add proxy for lamda invoke + fun.events.push({ + http: { + method: 'POST', + path: `{apiVersion}/functions/${fun.name}/invocations`, + integration: 'lambda', + request: { + template: { + // AWS SDK for NodeJS specifies as 'binary/octet-stream' not 'application/json' + 'binary/octet-stream': '$input.body', + }, + }, + response: { + headers: { + 'Content-Type': 'application/json', + }, + }, + }, + }); + + // Adds a route for each http endpoint + // eslint-disable-next-line + fun.events.forEach(event => { + + if (event.websocket) { + this.apiGatewayWebSocket._createWsAction(fun, funName, servicePath, funOptions, event); + + return; + } + + if (!event.http) return; + + this.apiGateway._createRoutes(event, funOptions, protectedRoutes, funName, servicePath, serviceRuntime, defaultContentType, key, fun); + }); + }); + } }; // Serverless exits with code 1 when a promise rejection is unhandled. Not AWS. From c51ea7ec6ed326304296d57878602bbf8c192d62 Mon Sep 17 00:00:00 2001 From: Daniel Nalborczyk Date: Sat, 15 Jun 2019 22:03:21 -0400 Subject: [PATCH 40/71] Property order nits --- src/ApiGateway.js | 26 +++++++++++++------------- src/ApiGatewayWebSocket.js | 4 ++-- src/index.js | 6 +++--- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/ApiGateway.js b/src/ApiGateway.js index cc7696bb8..238b4357f 100644 --- a/src/ApiGateway.js +++ b/src/ApiGateway.js @@ -55,19 +55,19 @@ module.exports = class ApiGateway { // HTTPS support if (typeof httpsDir === 'string' && httpsDir.length > 0) { serverOptions.tls = { - key: fs.readFileSync(path.resolve(httpsDir, 'key.pem'), 'ascii'), cert: fs.readFileSync(path.resolve(httpsDir, 'cert.pem'), 'ascii'), + key: fs.readFileSync(path.resolve(httpsDir, 'key.pem'), 'ascii'), }; } serverOptions.state = this.options.enforceSecureCookies ? { isHttpOnly: true, - isSecure: true, isSameSite: false, + isSecure: true, } : { isHttpOnly: false, - isSecure: false, isSameSite: false, + isSecure: false, }; // Hapijs server creation @@ -128,9 +128,9 @@ module.exports = class ApiGateway { if (!authFunction) return this.serverlessLog(`WARNING: Authorization function ${authFunctionName} does not exist`); const authorizerOptions = { - resultTtlInSeconds: '300', identitySource: 'method.request.header.Authorization', identityValidationExpression: '(.*)', + resultTtlInSeconds: '300', }; if (typeof endpoint.authorizer === 'string') { @@ -217,10 +217,10 @@ module.exports = class ApiGateway { let cors = null; if (endpoint.cors) { cors = { - origin: endpoint.cors.origins || this.options.corsConfig.origin, - headers: endpoint.cors.headers || this.options.corsConfig.headers, credentials: endpoint.cors.credentials || this.options.corsConfig.credentials, exposedHeaders: this.options.corsConfig.exposedHeaders, + headers: endpoint.cors.headers || this.options.corsConfig.headers, + origin: endpoint.cors.origins || this.options.corsConfig.origin, }; } @@ -228,18 +228,18 @@ module.exports = class ApiGateway { const routeMethod = method === 'ANY' ? '*' : method; const state = this.options.disableCookieValidation ? { - parse: false, failAction: 'ignore', + parse: false, } : { - parse: true, failAction: 'error', + parse: true, }; const routeConfig = { - cors, auth: authStrategyName, - timeout: { socket: false }, + cors, state, + timeout: { socket: false }, }; // skip HEAD routes as hapi will fail with 'Method name not allowed: HEAD ...' @@ -257,9 +257,9 @@ module.exports = class ApiGateway { } this.server.route({ + config: routeConfig, method: routeMethod, path: fullPath, - config: routeConfig, handler: (request, h) => { // Here we go // Payload processing const encoding = detectEncoding(request); @@ -867,9 +867,9 @@ module.exports = class ApiGateway { this.serverlessLog(`${method} ${fullPath} -> ${proxyUriInUse}`); this.server.route({ + config: routeConfig, method: routeMethod, path: fullPath, - config: routeConfig, handler: (request, h) => { const { params } = request; let resultUri = proxyUriInUse; @@ -895,9 +895,9 @@ module.exports = class ApiGateway { if (this.server.match('*', '/{p*}')) return; this.server.route({ + config: { cors: this.options.corsConfig }, method: '*', path: '/{p*}', - config: { cors: this.options.corsConfig }, handler: (request, h) => { const response = h.response({ statusCode: 404, diff --git a/src/ApiGatewayWebSocket.js b/src/ApiGatewayWebSocket.js index b566f3867..f4040c49f 100644 --- a/src/ApiGatewayWebSocket.js +++ b/src/ApiGatewayWebSocket.js @@ -54,12 +54,12 @@ module.exports = class ApiGatewayWebSocket { serverOptions.state = this.options.enforceSecureCookies ? { isHttpOnly: true, - isSecure: true, isSameSite: false, + isSecure: true, } : { isHttpOnly: false, - isSecure: false, isSameSite: false, + isSecure: false, }; // Hapijs server creation diff --git a/src/index.js b/src/index.js index 680f7febe..5ba1477ef 100755 --- a/src/index.js +++ b/src/index.js @@ -295,10 +295,10 @@ module.exports = class Offline { if (this.options.corsDisallowCredentials) this.options.corsAllowCredentials = false; this.options.corsConfig = { - origin: this.options.corsAllowOrigin, - headers: this.options.corsAllowHeaders, credentials: this.options.corsAllowCredentials, exposedHeaders: this.options.corsExposedHeaders, + headers: this.options.corsAllowHeaders, + origin: this.options.corsAllowOrigin, }; this.options.cacheInvalidationRegex = new RegExp(this.options.cacheInvalidationRegex); @@ -375,9 +375,9 @@ module.exports = class Offline { // Add proxy for lamda invoke fun.events.push({ http: { + integration: 'lambda', method: 'POST', path: `{apiVersion}/functions/${fun.name}/invocations`, - integration: 'lambda', request: { template: { // AWS SDK for NodeJS specifies as 'binary/octet-stream' not 'application/json' From 7b9c942ee7268756088490f480c7a2d8cb97b18f Mon Sep 17 00:00:00 2001 From: Daniel Nalborczyk Date: Mon, 17 Jun 2019 08:15:52 -0400 Subject: [PATCH 41/71] Spelling --- src/ApiGatewayWebSocket.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ApiGatewayWebSocket.js b/src/ApiGatewayWebSocket.js index f4040c49f..535433f85 100644 --- a/src/ApiGatewayWebSocket.js +++ b/src/ApiGatewayWebSocket.js @@ -96,13 +96,13 @@ module.exports = class ApiGatewayWebSocket { this.wsServer.register(require('hapi-plugin-websocket')).catch(err => err && this.serverlessLog(err)); - const doAction = (ws, connectionId, name, event, context, doDeafultAction/* , onError */) => { + const doAction = (ws, connectionId, name, event, context, doDefaultAction/* , onError */) => { const sendError = err => { if (ws.readyState === /* OPEN */1) ws.send(JSON.stringify({ message:'Internal server error', connectionId, requestId:'1234567890' })); debugLog(`Error in handler of action ${action}`, err); }; let action = this.wsActions[name]; - if (!action && doDeafultAction) action = this.wsActions.$default; + if (!action && doDefaultAction) action = this.wsActions.$default; if (!action) return; let p = null; try { From 5aa23650d7926d5af4a3c6263614fc1602d9ad2a Mon Sep 17 00:00:00 2001 From: Daniel Nalborczyk Date: Mon, 17 Jun 2019 08:20:07 -0400 Subject: [PATCH 42/71] Require on top of file --- src/ApiGatewayWebSocket.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ApiGatewayWebSocket.js b/src/ApiGatewayWebSocket.js index 535433f85..ee5d04c88 100644 --- a/src/ApiGatewayWebSocket.js +++ b/src/ApiGatewayWebSocket.js @@ -4,6 +4,7 @@ const fs = require('fs'); const path = require('path'); const hapi = require('@hapi/hapi'); const h2o2 = require('@hapi/h2o2'); +const hapiPluginWebsocket = require('hapi-plugin-websocket'); const debugLog = require('./debugLog'); const createAuthScheme = require('./createAuthScheme'); const functionHelper = require('./functionHelper'); @@ -94,7 +95,7 @@ module.exports = class ApiGatewayWebSocket { }); // end COPY PASTE FROM HTTP SERVER CODE - this.wsServer.register(require('hapi-plugin-websocket')).catch(err => err && this.serverlessLog(err)); + this.wsServer.register(hapiPluginWebsocket).catch(err => err && this.serverlessLog(err)); const doAction = (ws, connectionId, name, event, context, doDefaultAction/* , onError */) => { const sendError = err => { From 25832c81c55322deebbb678ccbc251e705d5cddb Mon Sep 17 00:00:00 2001 From: Daniel Nalborczyk Date: Mon, 17 Jun 2019 09:58:51 -0400 Subject: [PATCH 43/71] Use Lambda context for websocket --- src/ApiGatewayWebSocket.js | 31 ++++++++++++++++++++----------- src/websocketHelpers.js | 16 ---------------- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/src/ApiGatewayWebSocket.js b/src/ApiGatewayWebSocket.js index ee5d04c88..20c37eaed 100644 --- a/src/ApiGatewayWebSocket.js +++ b/src/ApiGatewayWebSocket.js @@ -6,6 +6,7 @@ const hapi = require('@hapi/hapi'); const h2o2 = require('@hapi/h2o2'); const hapiPluginWebsocket = require('hapi-plugin-websocket'); const debugLog = require('./debugLog'); +const createLambdaContext = require('./createLambdaContext') const createAuthScheme = require('./createAuthScheme'); const functionHelper = require('./functionHelper'); const { getUniqueId } = require('./utils'); @@ -97,7 +98,7 @@ module.exports = class ApiGatewayWebSocket { this.wsServer.register(hapiPluginWebsocket).catch(err => err && this.serverlessLog(err)); - const doAction = (ws, connectionId, name, event, context, doDefaultAction/* , onError */) => { + const doAction = (ws, connectionId, name, event, doDefaultAction/* , onError */) => { const sendError = err => { if (ws.readyState === /* OPEN */1) ws.send(JSON.stringify({ message:'Internal server error', connectionId, requestId:'1234567890' })); debugLog(`Error in handler of action ${action}`, err); @@ -105,12 +106,22 @@ module.exports = class ApiGatewayWebSocket { let action = this.wsActions[name]; if (!action && doDefaultAction) action = this.wsActions.$default; if (!action) return; + + function cb(err) { + if (!err) return; + sendError(err); + } + + // TEMP + const func = { + ...action.fun, + name, + }; + const context = createLambdaContext(func, this.service.provider, cb); + let p = null; try { - p = action.handler(event, context, err => { - if (!err) return; - sendError(err); - }); + p = action.handler(event, context, cb); } catch (err) { sendError(err); @@ -152,18 +163,16 @@ module.exports = class ApiGatewayWebSocket { this.clients.set(ws, connection); let event = wsHelpers.createConnectEvent('$connect', 'CONNECT', connection, this.options); if (Object.keys(queryStringParameters).length > 0) event = { queryStringParameters, ...event }; - const context = wsHelpers.createContext('$connect'); - doAction(ws, connection.connectionId, '$connect', event, context); + doAction(ws, connection.connectionId, '$connect', event, false); }, disconnect: ({ ws }) => { const connection = this.clients.get(ws); debugLog(`disconnect:${connection.connectionId}`); this.clients.delete(ws); const event = wsHelpers.createDisconnectEvent('$disconnect', 'DISCONNECT', connection, this.options); - const context = wsHelpers.createContext('$disconnect'); - doAction(ws, connection.connectionId, '$disconnect', event, context); + doAction(ws, connection.connectionId, '$disconnect', event, false); }, }, }, @@ -187,9 +196,8 @@ module.exports = class ApiGatewayWebSocket { const action = actionName || '$default'; debugLog(`action:${action} on connection=${connection.connectionId}`); const event = wsHelpers.createEvent(action, 'MESSAGE', connection, request.payload, this.options); - const context = wsHelpers.createContext(action); - doAction(ws, connection.connectionId, action, event, context, true); + doAction(ws, connection.connectionId, action, event, true); return h.response().code(204); }, @@ -261,6 +269,7 @@ module.exports = class ApiGatewayWebSocket { const actionName = event.websocket.route; const action = { funName, fun, funOptions, servicePath, handler }; + this.wsActions[actionName] = action; this.serverlessLog(`Action '${event.websocket.route}'`); } diff --git a/src/websocketHelpers.js b/src/websocketHelpers.js index 091212733..36805636a 100644 --- a/src/websocketHelpers.js +++ b/src/websocketHelpers.js @@ -112,19 +112,3 @@ exports.createDisconnectEvent = (action, eventType, connection, options) => { return event; }; - -exports.createContext = action => { - const context = { - awsRequestId: `offline_awsRequestId_for_${action}`, - callbackWaitsForEmptyEventLoop: true, - functionName: action, - functionVersion: '$LATEST', - invokedFunctionArn: `offline_invokedFunctionArn_for_${action}`, - invokeid: `offline_invokeid_for_${action}`, - logGroupName: `offline_logGroupName_for_${action}`, - logStreamName: `offline_logStreamName_for_${action}`, - memoryLimitInMB: '1024', - }; - - return context; -}; From 13869545453e9070ed5e0eae2031a054b2f2c313 Mon Sep 17 00:00:00 2001 From: Daniel Nalborczyk Date: Mon, 17 Jun 2019 10:14:03 -0400 Subject: [PATCH 44/71] Add warning for experimental WebSocket support --- src/index.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/index.js b/src/index.js index 5ba1477ef..45aca208a 100755 --- a/src/index.js +++ b/src/index.js @@ -397,6 +397,8 @@ module.exports = class Offline { fun.events.forEach(event => { if (event.websocket) { + experimentalWebSocketSupportWarning(); + this.apiGatewayWebSocket._createWsAction(fun, funName, servicePath, funOptions, event); return; @@ -410,6 +412,24 @@ module.exports = class Offline { } }; +let experimentalNotified = false; + +function experimentalWebSocketSupportWarning() { + // notify only once + if (experimentalNotified) { + return; + } + + const warning = ` + WebSocket support in "serverless-offline" is experimental. + For any bugs, missing features, or other feedback file an issue at https://github.com/dherault/serverless-offline/issues . + `; + + console.warn(warning); + + experimentalNotified = true; +} + // Serverless exits with code 1 when a promise rejection is unhandled. Not AWS. // Users can still use their own unhandledRejection event though. process.removeAllListeners('unhandledRejection'); From 35a361620b33bb30b20177eebcb3916c482c8e10 Mon Sep 17 00:00:00 2001 From: Daniel Nalborczyk Date: Mon, 17 Jun 2019 10:16:35 -0400 Subject: [PATCH 45/71] Linting --- src/ApiGatewayWebSocket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ApiGatewayWebSocket.js b/src/ApiGatewayWebSocket.js index 20c37eaed..58c64fefd 100644 --- a/src/ApiGatewayWebSocket.js +++ b/src/ApiGatewayWebSocket.js @@ -6,7 +6,7 @@ const hapi = require('@hapi/hapi'); const h2o2 = require('@hapi/h2o2'); const hapiPluginWebsocket = require('hapi-plugin-websocket'); const debugLog = require('./debugLog'); -const createLambdaContext = require('./createLambdaContext') +const createLambdaContext = require('./createLambdaContext'); const createAuthScheme = require('./createAuthScheme'); const functionHelper = require('./functionHelper'); const { getUniqueId } = require('./utils'); From ca165069377412e343dcd4a1d139208f0808759d Mon Sep 17 00:00:00 2001 From: Daniel Nalborczyk Date: Mon, 17 Jun 2019 10:17:23 -0400 Subject: [PATCH 46/71] Remove unused this.clients --- src/ApiGateway.js | 1 - src/index.js | 1 - 2 files changed, 2 deletions(-) diff --git a/src/ApiGateway.js b/src/ApiGateway.js index 238b4357f..adee630cb 100644 --- a/src/ApiGateway.js +++ b/src/ApiGateway.js @@ -26,7 +26,6 @@ module.exports = class ApiGateway { this.serverlessLog = serverless.cli.log.bind(serverless.cli); this.options = options; this.exitCode = 0; - this.clients = new Map(); this.requests = {}; this.velocityContextOptions = velocityContextOptions; diff --git a/src/index.js b/src/index.js index 45aca208a..a21e328c8 100755 --- a/src/index.js +++ b/src/index.js @@ -15,7 +15,6 @@ module.exports = class Offline { this.serverlessLog = serverless.cli.log.bind(serverless.cli); this.options = options; this.exitCode = 0; - this.clients = new Map(); this.commands = { offline: { From d6314586d2aed2ec38cca218d4dbaee5d1a8c4de Mon Sep 17 00:00:00 2001 From: Daniel Nalborczyk Date: Mon, 17 Jun 2019 10:22:22 -0400 Subject: [PATCH 47/71] Rename variable --- src/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/index.js b/src/index.js index a21e328c8..3b6d151f3 100755 --- a/src/index.js +++ b/src/index.js @@ -411,22 +411,22 @@ module.exports = class Offline { } }; -let experimentalNotified = false; +let experimentalWarningNotified = false; function experimentalWebSocketSupportWarning() { // notify only once - if (experimentalNotified) { + if (experimentalWarningNotified) { return; } const warning = ` WebSocket support in "serverless-offline" is experimental. - For any bugs, missing features, or other feedback file an issue at https://github.com/dherault/serverless-offline/issues . + For any bugs, missing features or other feedback file an issue at https://github.com/dherault/serverless-offline/issues . `; console.warn(warning); - experimentalNotified = true; + experimentalWarningNotified = true; } // Serverless exits with code 1 when a promise rejection is unhandled. Not AWS. From 0e3b4d8b1abf731ed2dc363f1b4f4ae7ce7b0a5c Mon Sep 17 00:00:00 2001 From: Daniel Nalborczyk Date: Tue, 18 Jun 2019 12:15:05 -0400 Subject: [PATCH 48/71] Add websocket to package.json keywords --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 37828028d..0396efc63 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "amazon web services", "aws", "lambda", + "websocket", "api gateway" ], "files": [ From 741287b454d5202eee2bb31b2b31e52b3899acad Mon Sep 17 00:00:00 2001 From: Daniel Nalborczyk Date: Tue, 18 Jun 2019 12:27:58 -0400 Subject: [PATCH 49/71] Update deps --- package-lock.json | 221 ++++++++++++++++++++++++---------------------- package.json | 6 +- 2 files changed, 117 insertions(+), 110 deletions(-) diff --git a/package-lock.json b/package-lock.json index e1ad4015c..95f70a111 100644 --- a/package-lock.json +++ b/package-lock.json @@ -396,9 +396,9 @@ } }, "@serverless/enterprise-plugin": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@serverless/enterprise-plugin/-/enterprise-plugin-1.0.5.tgz", - "integrity": "sha512-4n15uV5ka/PQWh0jUKg6s8qGmqkL4gB9QsDdIRO3ENEaOwKkO2CVa0FdsVZ1HmKDbDknnYflwm2jv3tDI+VGKA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@serverless/enterprise-plugin/-/enterprise-plugin-1.0.7.tgz", + "integrity": "sha512-v7QXnZ60bsc0BvCIKg9WpooRYOMfh/F6U3VKJj2uM04PCM50BRsix/HnjLHJSvH4OyEu0EOgjOxXTeXjoDff4w==", "dev": true, "requires": { "@serverless/event-mocks": "^1.1.1", @@ -407,6 +407,7 @@ "flat": "^4.1.0", "fs-extra": "^7.0.1", "iso8601-duration": "^1.1.7", + "isomorphic-fetch": "^2.2.1", "jsonata": "^1.6.4", "jszip": "^3.2.1", "lodash": "^4.17.11", @@ -510,9 +511,9 @@ } }, "@sinonjs/samsam": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.1.tgz", - "integrity": "sha512-wRSfmyd81swH0hA1bxJZJ57xr22kC07a1N4zuIL47yTS04bDk6AoCkczcqHEjcRPmJ+FruGJ9WBQiJwMtIElFw==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.2.tgz", + "integrity": "sha512-ILO/rR8LfAb60Y1Yfp9vxfYAASK43NFC2mLzpvLUbCQY/Qu8YwReboseu8aheCEkyElZF2L2T9mHcR2bgdvZyA==", "dev": true, "requires": { "@sinonjs/commons": "^1.0.2", @@ -527,9 +528,9 @@ "dev": true }, "@types/lodash": { - "version": "4.14.133", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.133.tgz", - "integrity": "sha512-/3JqnvPnY58GLzG3Y7fpphOhATV1DDZ/Ak3DQufjlRK5E4u+s0CfClfNFtAGBabw+jDGtRFbOZe+Z02ZMWCBNQ==", + "version": "4.14.134", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.134.tgz", + "integrity": "sha512-2/O0khFUCFeDlbi7sZ7ZFRCcT812fAeOLm7Ev4KbwASkZ575TDrDcY7YyaoHdTOzKcNbfiwLYZqPmoC4wadrsw==", "dev": true }, "accepts": { @@ -803,9 +804,9 @@ "dev": true }, "aws-sdk": { - "version": "2.469.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.469.0.tgz", - "integrity": "sha512-VaIrO3aBX83gKkBPk9xM0RHmu7fmq76kaF0SqbsWlPImgxc5foJ4rBlRMMlmeNogFZZ/XTQdI+gkFDVosV14Ig==", + "version": "2.477.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.477.0.tgz", + "integrity": "sha512-CdUbQg0tIIydyCa+rUMzS7EuWV6hUQmSp6gWYy/hdsyeh+fpARjpijrerAML1iawsC47TKTgT5lYE97Kf6DTxg==", "dev": true, "requires": { "buffer": "4.9.1", @@ -913,9 +914,9 @@ } }, "babel-eslint": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.0.1.tgz", - "integrity": "sha512-z7OT1iNV+TjOwHNLLyJk+HN+YVWX+CLE6fPD2SymJZOZQBs+QIexFjhm4keGTm8MW9xr4EC9Q0PbaLB24V5GoQ==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.0.2.tgz", + "integrity": "sha512-UdsurWPtgiPgpJ06ryUnuaSXC2s0WoSZnQmEpbAH65XZSdwowgN5MvyP7e88nW07FYXv72erVtpBkxyDVKhH1Q==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -1117,14 +1118,6 @@ } } }, - "boom": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-7.3.0.tgz", - "integrity": "sha512-Swpoyi2t5+GhOEGw8rEsKvTxFLIDiiKoUc2gsoV6Lyr43LHBIzch3k2MvYUs8RTROrIkVJ3Al0TkaOGjnb+B6A==", - "requires": { - "hoek": "6.x.x" - } - }, "boxen": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", @@ -2329,9 +2322,9 @@ } }, "es6-promise": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.6.tgz", - "integrity": "sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", "dev": true }, "es6-promisify": { @@ -2663,9 +2656,9 @@ "integrity": "sha512-HnSYx1BsJ87/p6swwzv+2v6B4X+uxUteoDfRxsAb1S1BePzQqOLevVmkdA15GHJVd9A9Ok6wygUR18Hu0YeV9g==" }, "eventemitter3": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", - "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" }, "events": { "version": "1.1.1", @@ -2755,12 +2748,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", - "dev": true } } }, @@ -3222,15 +3209,22 @@ "dev": true }, "hapi-plugin-websocket": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/hapi-plugin-websocket/-/hapi-plugin-websocket-2.0.16.tgz", - "integrity": "sha512-UE2JSrNIalXJmw4Qoi2FqhcguqTYQhjgPAVCtJUBhhZXD6IyA15guTdSeshBLxGfKcAFJmlpXs2LUMB25ARfOA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/hapi-plugin-websocket/-/hapi-plugin-websocket-2.1.2.tgz", + "integrity": "sha512-QqpBaWJPeAq/uWd3pNt73hoavMiJJgW8agOBPMlYuT3qowrgnVWSZeAd5Gtv+7kG/Yk6cV0Vg26RBMNohe0jBA==", "requires": { - "boom": "7.3.0", - "hoek": "6.1.2", + "@hapi/boom": "7.4.2", + "@hapi/hoek": "7.1.0", "urijs": "1.19.1", - "websocket-framed": "1.2.0", - "ws": "6.1.2" + "websocket-framed": "1.2.1", + "ws": "7.0.0" + }, + "dependencies": { + "@hapi/hoek": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-7.1.0.tgz", + "integrity": "sha512-jBTPzWrWQAizq7naLVwU+P2+TzVY3ZtPSX+F9gwW23ihwpihpYKvjN21zHKUjaePYS9ijlDF3oFVNbGfhbbk2w==" + } } }, "has": { @@ -3355,11 +3349,6 @@ } } }, - "hoek": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.2.tgz", - "integrity": "sha512-6qhh/wahGYZHFSFw12tBbJw5fsAhhwrrG/y3Cs0YMTv2WzMnL0oLPnQJjv1QJvEfylRSOFuP+xCu+tdx0tD16Q==" - }, "homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -3938,9 +3927,9 @@ } }, "jsonpath-plus": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-0.20.0.tgz", - "integrity": "sha512-iJvK4A9R/h1j/SRo2zIx0SsIiEa9u38prp8/RZrV6XBJtBc1f9TEfT37CO34FzFp4LOED1G/H4zUZad51RMDfA==" + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-0.20.1.tgz", + "integrity": "sha512-8O4tBeXh9XGma2x2aPVwvpo9lXJAd4bx0XA0eRjYs4Cpz7e5PQy7sPttk2YmhvROJhEUu4DNUxmtLueicCqyZg==" }, "jsonschema": { "version": "1.2.4", @@ -4571,6 +4560,12 @@ "path-exists": "^3.0.0" } }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, "p-limit": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", @@ -4613,9 +4608,9 @@ "dev": true }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "msgpack-lite": { "version": "0.1.26", @@ -4688,6 +4683,23 @@ "just-extend": "^4.0.2", "lolex": "^4.1.0", "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + } } }, "node-dir": { @@ -5466,21 +5478,10 @@ "dev": true }, "path-to-regexp": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", - "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", - "dev": true, - "requires": { - "isarray": "0.0.1" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - } - } + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true }, "path-type": { "version": "2.0.0", @@ -5866,9 +5867,9 @@ "dev": true }, "resolve": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz", - "integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.0.tgz", + "integrity": "sha512-WL2pBDjqT6pGUNSUzMw00o4T7If+z4H2x3Gz893WoUQ5KW8Vr9txp00ykiP16VBaZF5+j/OcXJHZ9+PCvdiDKw==", "dev": true, "requires": { "path-parse": "^1.0.6" @@ -6054,6 +6055,12 @@ "dev": true } } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true } } }, @@ -6070,51 +6077,51 @@ } }, "serverless": { - "version": "1.44.1", - "resolved": "https://registry.npmjs.org/serverless/-/serverless-1.44.1.tgz", - "integrity": "sha512-xbAtKobCPb67ovst5FsNt0eBlymESiISjJ/KOS4KrJCfwOJNJfDHIG07VwiKC1aJ5aQZX2V1Kf++DDrzbBxBng==", + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/serverless/-/serverless-1.45.1.tgz", + "integrity": "sha512-V51kAYdytoQo/RnTyswneO6zYyFOslQkJvnPOjqVAd7wXPAVgxNjY0LF035ZkEZtNysxbto1Ur1CmNxactNpGA==", "dev": true, "requires": { "@serverless/enterprise-plugin": "^1.0.3", - "archiver": "^1.1.0", + "archiver": "^1.3.0", "async": "^1.5.2", - "aws-sdk": "^2.430.0", - "bluebird": "^3.5.0", + "aws-sdk": "^2.473.0", + "bluebird": "^3.5.5", "cachedir": "^2.2.0", - "chalk": "^2.0.0", - "ci-info": "^1.1.1", - "download": "^5.0.2", + "chalk": "^2.4.2", + "ci-info": "^1.6.0", + "download": "^5.0.3", "fast-levenshtein": "^2.0.6", - "filesize": "^3.3.0", + "filesize": "^3.6.1", "fs-extra": "^0.26.7", "get-stdin": "^5.0.1", "globby": "^6.1.0", - "graceful-fs": "^4.1.11", + "graceful-fs": "^4.1.15", "https-proxy-agent": "^2.2.1", "is-docker": "^1.1.0", - "js-yaml": "^3.13.0", + "js-yaml": "^3.13.1", "json-cycle": "^1.3.0", - "json-refs": "^2.1.5", + "json-refs": "^2.1.7", "jszip": "^3.2.1", "jwt-decode": "^2.2.0", - "lodash": "^4.13.1", + "lodash": "^4.17.11", "minimist": "^1.2.0", "mkdirp": "^0.5.1", - "moment": "^2.13.0", + "moment": "^2.24.0", "nanomatch": "^1.2.13", - "node-fetch": "^1.6.0", - "object-hash": "^1.2.0", - "promise-queue": "^2.2.3", + "node-fetch": "^1.7.3", + "object-hash": "^1.3.1", + "promise-queue": "^2.2.5", "raven": "^1.2.1", - "rc": "^1.1.6", + "rc": "^1.2.8", "replaceall": "^0.1.6", "semver": "^5.7.0", "semver-regex": "^1.0.0", "tabtab": "^2.2.2", "untildify": "^3.0.3", - "update-notifier": "^2.2.0", - "uuid": "^2.0.2", - "write-file-atomic": "^2.1.0", + "update-notifier": "^2.5.0", + "uuid": "^2.0.3", + "write-file-atomic": "^2.4.3", "yaml-ast-parser": "0.0.34" }, "dependencies": { @@ -6679,9 +6686,9 @@ } }, "table": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.0.tgz", - "integrity": "sha512-nHFDrxmbrkU7JAFKqKbDJXfzrX2UBsWmrieXFTGxiI5e4ncg3VqsZeI4EzNmX0ncp4XNGVeoxIWJXfCIXwrsvw==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.1.tgz", + "integrity": "sha512-E6CK1/pZe2N75rGZQotFOdmzWQ1AILtgYbMAbAjvms0S1l5IDB47zG3nCnFGB/w+7nB3vKofbLXCH7HPBo864w==", "dev": true, "requires": { "ajv": "^6.9.1", @@ -7074,9 +7081,9 @@ "dev": true }, "tslib": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", - "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", "dev": true }, "tunnel-agent": { @@ -7371,12 +7378,12 @@ "dev": true }, "websocket-framed": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/websocket-framed/-/websocket-framed-1.2.0.tgz", - "integrity": "sha512-rnVf9NisrPAKIzB0LLgLdnbiElRNZSeahKKXcicxzOxJdW4ZaCE7xR7nheBIoN2j++2fk6FD1GTg9d+rw/X9+g==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/websocket-framed/-/websocket-framed-1.2.1.tgz", + "integrity": "sha512-Gzny2xBIboB/gO8ZIP2gRZQz5x0S+kxyJwBXvGhrbwolNVG5i4THm1IdA+0ga9ZWTpLCHrkcFXB+s88TlkDJUQ==", "requires": { "encodr": "1.2.0", - "eventemitter3": "3.1.0" + "eventemitter3": "3.1.2" } }, "whatwg-fetch": { @@ -7508,11 +7515,11 @@ } }, "ws": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.2.tgz", - "integrity": "sha512-rfUqzvz0WxmSXtJpPMX2EeASXabOrSMk1ruMOV3JBTBjo4ac2lDjGGsbQSyxj8Odhw5fBib8ZKEjDNvgouNKYw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.0.0.tgz", + "integrity": "sha512-cknCal4k0EAOrh1SHHPPWWh4qm93g1IuGGGwBjWkXmCG7LsDtL8w9w+YVfaF+KSVwiHQKDIMsSLBVftKf9d1pg==", "requires": { - "async-limiter": "~1.0.0" + "async-limiter": "^1.0.0" } }, "xdg-basedir": { diff --git a/package.json b/package.json index 0396efc63..dfdd50320 100644 --- a/package.json +++ b/package.json @@ -135,9 +135,9 @@ "@hapi/h2o2": "^8.3.0", "@hapi/hapi": "^18.3.1", "cuid": "^2.1.6", - "hapi-plugin-websocket": "^2.0.16", + "hapi-plugin-websocket": "^2.1.2", "js-string-escape": "^1.0.1", - "jsonpath-plus": "^0.20.0", + "jsonpath-plus": "^0.20.1", "jsonschema": "^1.2.4", "jsonwebtoken": "^8.5.1", "luxon": "^1.16.0", @@ -150,7 +150,7 @@ "eslint-config-dherault": "^1.0.2", "mocha": "^6.1.4", "npm-check": "^5.9.0", - "serverless": "^1.44.1", + "serverless": "^1.45.1", "sinon": "^7.3.2" }, "peerDependencies": { From d0fe602128617ee03dc6db0c936e907ff34f4ce0 Mon Sep 17 00:00:00 2001 From: Daniel Nalborczyk Date: Tue, 18 Jun 2019 12:43:26 -0400 Subject: [PATCH 50/71] Change minimum serverless peer dep to v1.39.0 for websocket support --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dfdd50320..f965f4fa8 100644 --- a/package.json +++ b/package.json @@ -154,6 +154,6 @@ "sinon": "^7.3.2" }, "peerDependencies": { - "serverless": ">= 1.0.0 < 2.0.0" + "serverless": ">= 1.39.0 < 2.0.0" } } From e54cdc33da095bebd6c6bf0d2cc502bb7549b072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20H=C3=A9rault?= Date: Sat, 22 Jun 2019 18:41:22 +0200 Subject: [PATCH 51/71] Lint and rename folders --- .eslintignore | 5 +- .eslintrc.js | 4 + manual_test_nodejs/handler.js | 3 +- manual_test_nodejs/subprocess.js | 1 - .../RouteSelection/handler.js | 31 ++ .../package-lock.json | 0 .../package.json | 0 .../scripts/deploy_to_aws.sh | 0 .../scripts/deploy_to_offline.sh | 0 .../scripts/serverless..yml | 0 .../scripts/serverless.aws.yml | 0 .../scripts/serverless.offline.yml | 0 .../serverless.yml | 0 .../RouteSelection/test/e2e/ws.e2e.js | 59 +++ .../test/support/WebSocketTester.js | 62 +++ manual_test_websocket/main/handler.js | 131 +++++++ .../package-lock.json | 0 .../package.json | 0 .../scripts/deploy_to_aws.sh | 0 .../scripts/deploy_to_offline.sh | 0 .../scripts/serverless..yml | 0 .../scripts/serverless.aws.yml | 0 .../scripts/serverless.offline.yml | 0 .../serverless.yml | 0 manual_test_websocket/main/test/e2e/ws.e2e.js | 360 ++++++++++++++++++ .../main/test/support/WebSocketTester.js | 62 +++ .../handler.js | 37 -- .../test/e2e/ws.e2e.js | 53 --- .../test/support/WebSocketTester.js | 62 --- .../manual_test_websocket_main/handler.js | 150 -------- .../test/e2e/ws.e2e.js | 342 ----------------- .../test/support/WebSocketTester.js | 60 --- 32 files changed, 711 insertions(+), 711 deletions(-) create mode 100644 manual_test_websocket/RouteSelection/handler.js rename manual_test_websocket/{manual_test_websocket_RouteSelection => RouteSelection}/package-lock.json (100%) rename manual_test_websocket/{manual_test_websocket_RouteSelection => RouteSelection}/package.json (100%) rename manual_test_websocket/{manual_test_websocket_RouteSelection => RouteSelection}/scripts/deploy_to_aws.sh (100%) rename manual_test_websocket/{manual_test_websocket_RouteSelection => RouteSelection}/scripts/deploy_to_offline.sh (100%) rename manual_test_websocket/{manual_test_websocket_RouteSelection => RouteSelection}/scripts/serverless..yml (100%) rename manual_test_websocket/{manual_test_websocket_RouteSelection => RouteSelection}/scripts/serverless.aws.yml (100%) rename manual_test_websocket/{manual_test_websocket_RouteSelection => RouteSelection}/scripts/serverless.offline.yml (100%) rename manual_test_websocket/{manual_test_websocket_RouteSelection => RouteSelection}/serverless.yml (100%) create mode 100644 manual_test_websocket/RouteSelection/test/e2e/ws.e2e.js create mode 100644 manual_test_websocket/RouteSelection/test/support/WebSocketTester.js create mode 100644 manual_test_websocket/main/handler.js rename manual_test_websocket/{manual_test_websocket_main => main}/package-lock.json (100%) rename manual_test_websocket/{manual_test_websocket_main => main}/package.json (100%) rename manual_test_websocket/{manual_test_websocket_main => main}/scripts/deploy_to_aws.sh (100%) rename manual_test_websocket/{manual_test_websocket_main => main}/scripts/deploy_to_offline.sh (100%) rename manual_test_websocket/{manual_test_websocket_main => main}/scripts/serverless..yml (100%) rename manual_test_websocket/{manual_test_websocket_main => main}/scripts/serverless.aws.yml (100%) rename manual_test_websocket/{manual_test_websocket_main => main}/scripts/serverless.offline.yml (100%) rename manual_test_websocket/{manual_test_websocket_main => main}/serverless.yml (100%) create mode 100644 manual_test_websocket/main/test/e2e/ws.e2e.js create mode 100644 manual_test_websocket/main/test/support/WebSocketTester.js delete mode 100644 manual_test_websocket/manual_test_websocket_RouteSelection/handler.js delete mode 100644 manual_test_websocket/manual_test_websocket_RouteSelection/test/e2e/ws.e2e.js delete mode 100644 manual_test_websocket/manual_test_websocket_RouteSelection/test/support/WebSocketTester.js delete mode 100644 manual_test_websocket/manual_test_websocket_main/handler.js delete mode 100644 manual_test_websocket/manual_test_websocket_main/test/e2e/ws.e2e.js delete mode 100644 manual_test_websocket/manual_test_websocket_main/test/support/WebSocketTester.js diff --git a/.eslintignore b/.eslintignore index 211df6524..cf7098890 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1 @@ -manual_test_nodejs -manual_test_python -manual_test_ruby -manual_test_websocket +**/node_modules diff --git a/.eslintrc.js b/.eslintrc.js index 5c7d6c559..cf14cb592 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -21,4 +21,8 @@ if (env.TRAVIS && platform === 'win32') { module.exports = { extends: 'dherault', rules, + env: { + node: true, + mocha: true, + }, }; diff --git a/manual_test_nodejs/handler.js b/manual_test_nodejs/handler.js index 568b4dc91..12b454a26 100644 --- a/manual_test_nodejs/handler.js +++ b/manual_test_nodejs/handler.js @@ -1,4 +1,3 @@ -'use strict'; module.exports.hello = (event, context, callback) => { const response = { @@ -28,7 +27,7 @@ module.exports.rejectedPromise = (event, context, callback) => { callback(null, response); }; -module.exports.authFunction = (event, context, callback) => { +module.exports.authFunction = (event, context) => { context.succeed({ principalId: 'xxxxxxx', // the principal user identification associated with the token send by the client policyDocument: { diff --git a/manual_test_nodejs/subprocess.js b/manual_test_nodejs/subprocess.js index 913220774..7794c64f7 100644 --- a/manual_test_nodejs/subprocess.js +++ b/manual_test_nodejs/subprocess.js @@ -1,4 +1,3 @@ -'use strict'; const { exec } = require('child_process'); diff --git a/manual_test_websocket/RouteSelection/handler.js b/manual_test_websocket/RouteSelection/handler.js new file mode 100644 index 000000000..d43ffebc5 --- /dev/null +++ b/manual_test_websocket/RouteSelection/handler.js @@ -0,0 +1,31 @@ +const AWS = require('aws-sdk'); + +const successfullResponse = { + statusCode: 200, + body: 'Request is OK.', +}; + +module.exports.echo = async (event, context) => { + const action = JSON.parse(event.body); + + await sendToClient(action.message, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)); + + return successfullResponse; +}; + +const newAWSApiGatewayManagementApi = event => { + let endpoint = event.apiGatewayUrl; + + if (!endpoint) endpoint = `${event.requestContext.domainName}/${event.requestContext.stage}`; + const apiVersion = '2018-11-29'; + + return new AWS.ApiGatewayManagementApi({ apiVersion, endpoint }); +}; + +const sendToClient = (data, connectionId, apigwManagementApi) => { + // console.log(`sendToClient:${connectionId}`); + let sendee = data; + if (typeof data === 'object') sendee = JSON.stringify(data); + + return apigwManagementApi.postToConnection({ ConnectionId: connectionId, Data: sendee }).promise(); +}; diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/package-lock.json b/manual_test_websocket/RouteSelection/package-lock.json similarity index 100% rename from manual_test_websocket/manual_test_websocket_RouteSelection/package-lock.json rename to manual_test_websocket/RouteSelection/package-lock.json diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/package.json b/manual_test_websocket/RouteSelection/package.json similarity index 100% rename from manual_test_websocket/manual_test_websocket_RouteSelection/package.json rename to manual_test_websocket/RouteSelection/package.json diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/deploy_to_aws.sh b/manual_test_websocket/RouteSelection/scripts/deploy_to_aws.sh similarity index 100% rename from manual_test_websocket/manual_test_websocket_RouteSelection/scripts/deploy_to_aws.sh rename to manual_test_websocket/RouteSelection/scripts/deploy_to_aws.sh diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/deploy_to_offline.sh b/manual_test_websocket/RouteSelection/scripts/deploy_to_offline.sh similarity index 100% rename from manual_test_websocket/manual_test_websocket_RouteSelection/scripts/deploy_to_offline.sh rename to manual_test_websocket/RouteSelection/scripts/deploy_to_offline.sh diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless..yml b/manual_test_websocket/RouteSelection/scripts/serverless..yml similarity index 100% rename from manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless..yml rename to manual_test_websocket/RouteSelection/scripts/serverless..yml diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless.aws.yml b/manual_test_websocket/RouteSelection/scripts/serverless.aws.yml similarity index 100% rename from manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless.aws.yml rename to manual_test_websocket/RouteSelection/scripts/serverless.aws.yml diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless.offline.yml b/manual_test_websocket/RouteSelection/scripts/serverless.offline.yml similarity index 100% rename from manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless.offline.yml rename to manual_test_websocket/RouteSelection/scripts/serverless.offline.yml diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/serverless.yml b/manual_test_websocket/RouteSelection/serverless.yml similarity index 100% rename from manual_test_websocket/manual_test_websocket_RouteSelection/serverless.yml rename to manual_test_websocket/RouteSelection/serverless.yml diff --git a/manual_test_websocket/RouteSelection/test/e2e/ws.e2e.js b/manual_test_websocket/RouteSelection/test/e2e/ws.e2e.js new file mode 100644 index 000000000..fcd11c0be --- /dev/null +++ b/manual_test_websocket/RouteSelection/test/e2e/ws.e2e.js @@ -0,0 +1,59 @@ +/* eslint-disable import/no-extraneous-dependencies */ + +const chai = require('chai'); + +const WebSocketTester = require('../support/WebSocketTester'); + +const expect = chai.expect; +const endpoint = process.env.npm_config_endpoint || 'ws://localhost:3005'; +const timeout = process.env.npm_config_timeout ? parseInt(process.env.npm_config_timeout) : 1000; + +describe('serverless', () => { + describe('with WebSocket support', () => { + let clients = []; + + const createWebSocket = async qs => { + const ws = new WebSocketTester(); + let url = endpoint; + + if (qs) url = `${endpoint}?${qs}`; + + await ws.open(url); + + clients.push(ws); + + return ws; + }; + + beforeEach(() => { + clients = []; + }); + + afterEach(async () => { + await Promise.all(clients.map(async (ws, i) => { + const n = ws.countUnrecived(); + + if (n > 0) { + console.log(`unreceived:[i=${i}]`); + (await ws.receive(n)).forEach(m => console.log(m)); + } + + expect(n).to.equal(0); + ws.close(); + })); + + clients = []; + }); + + it('should call action \'echo\' handler located at service.do', async () => { + const ws = await createWebSocket(); + const now = `${Date.now()}`; + const payload = JSON.stringify({ service:{ do:'echo' }, message:now }); + + ws.send(payload); + + expect(await ws.receive1()).to.equal(`${now}`); + }).timeout(timeout); + + }); +}); diff --git a/manual_test_websocket/RouteSelection/test/support/WebSocketTester.js b/manual_test_websocket/RouteSelection/test/support/WebSocketTester.js new file mode 100644 index 000000000..aaeff5a4a --- /dev/null +++ b/manual_test_websocket/RouteSelection/test/support/WebSocketTester.js @@ -0,0 +1,62 @@ +/* eslint-disable import/no-extraneous-dependencies */ +const WebSocket = require('ws'); + +class WebSocketTester { + constructor() { + this.messages = []; this.receivers = []; + } + + open(url) { + if (this.ws != null) return; + const ws = this.ws = new WebSocket(url); + ws.on('message', message => { + // console.log('Received: '+message); + if (this.receivers.length > 0) this.receivers.shift()(message); + else this.messages.push(message); + }); + + return new Promise(resolve => { + ws.on('open', () => { + resolve(true); + }); + }); + } + + send(data) { + this.ws.send(data); + } + + receive1() { + return new Promise(resolve => { + if (this.messages.length > 0) resolve(this.messages.shift()); + else this.receivers.push(resolve); + }); + } + + receive(n) { + return new Promise(resolve => { + const messages = []; + for (let i = 0; i < n; i += 1) { + this.receive1().then(message => { + messages[i] = message; + if (i === n - 1) resolve(messages); + }); + } + }); + } + + skip() { + if (this.messages.length > 0) this.messages.shift(); + else this.receivers.push(() => {}); + } + + countUnrecived() { + return this.messages.length; + } + + close() { + if (this.ws != null) this.ws.close(); + } +} + +module.exports = WebSocketTester; diff --git a/manual_test_websocket/main/handler.js b/manual_test_websocket/main/handler.js new file mode 100644 index 000000000..58c4d7d47 --- /dev/null +++ b/manual_test_websocket/main/handler.js @@ -0,0 +1,131 @@ +const AWS = require('aws-sdk'); + +const ddb = (() => { + if (process.env.IS_OFFLINE) return new AWS.DynamoDB.DocumentClient({ region: 'localhost', endpoint: 'http://localhost:8000' }); + + return new AWS.DynamoDB.DocumentClient(); +})(); + +const successfullResponse = { + statusCode: 200, + body: 'Request is OK.', +}; + +module.exports.connect = async (event, context) => { + // console.log('connect:'); + const listener = await ddb.get({ TableName:'listeners', Key:{ name:'default' } }).promise(); + + if (listener.Item) { + const timeout = new Promise(resolve => setTimeout(resolve, 100)); + const send = sendToClient( // sendToClient won't return on AWS when client doesn't exits so we set a timeout + JSON.stringify({ action:'update', event:'connect', info:{ id:event.requestContext.connectionId, event:{ ...event, apiGatewayUrl:`${event.apiGatewayUrl}` }, context } }), + listener.Item.id, + newAWSApiGatewayManagementApi(event, context)).catch(() => {}); + await Promise.race([send, timeout]); + } + + return successfullResponse; +}; + +// module.export.auth = (event, context, callback) => { +// //console.log('auth:'); +// const token = event.headers["Authorization"]; + +// if ('deny'===token) callback(null, generatePolicy('user', 'Deny', event.methodArn)); +// else callback(null, generatePolicy('user', 'Allow', event.methodArn));; +// }; + +module.exports.disconnect = async (event, context) => { + const listener = await ddb.get({ TableName:'listeners', Key:{ name:'default' } }).promise(); + if (listener.Item) await sendToClient(JSON.stringify({ action:'update', event:'disconnect', info:{ id:event.requestContext.connectionId, event:{ ...event, apiGatewayUrl:`${event.apiGatewayUrl}` }, context } }), listener.Item.id, newAWSApiGatewayManagementApi(event, context)).catch(() => {}); + + return successfullResponse; +}; + +module.exports.defaultHandler = async (event, context) => { + await sendToClient(`Error: No Supported Action in Payload '${event.body}'`, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err => console.log(err)); + + return successfullResponse; +}; + +module.exports.getClientInfo = async (event, context) => { + // console.log('getClientInfo:'); + await sendToClient({ action:'update', event:'client-info', info:{ id:event.requestContext.connectionId } }, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err => console.log(err)); + + return successfullResponse; +}; + +module.exports.getCallInfo = async (event, context) => { + await sendToClient({ action:'update', event:'call-info', info:{ event:{ ...event, apiGatewayUrl:`${event.apiGatewayUrl}` }, context } }, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err => console.log(err)); + + return successfullResponse; +}; + +module.exports.makeError = async () => { + const obj = null; + obj.non.non = 1; + + return successfullResponse; +}; + +module.exports.replyViaCallback = (event, context, callback) => { + sendToClient({ action:'update', event:'reply-via-callback' }, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err => console.log(err)); + callback(); +}; + +module.exports.replyErrorViaCallback = (event, context, callback) => callback('error error error'); + +module.exports.multiCall1 = async (event, context) => { + await sendToClient({ action:'update', event:'made-call-1' }, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err => console.log(err)); + + return successfullResponse; +}; + +module.exports.multiCall2 = async (event, context) => { + await sendToClient({ action:'update', event:'made-call-2' }, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err => console.log(err)); + + return successfullResponse; +}; + +module.exports.send = async (event, context) => { + const action = JSON.parse(event.body); + const sents = []; + action.clients.forEach(connectionId => { + const sent = sendToClient(action.data, connectionId, newAWSApiGatewayManagementApi(event, context)); + sents.push(sent); + }); + const noErr = await Promise.all(sents).then(() => true).catch(() => false); + if (!noErr) await sendToClient('Error: Could not Send all Messages', event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)); + + return successfullResponse; +}; + +module.exports.registerListener = async (event, context) => { + await ddb.put({ TableName:'listeners', Item:{ name:'default', id:event.requestContext.connectionId } }).promise(); + await sendToClient({ action:'update', event:'register-listener', info:{ id:event.requestContext.connectionId } }, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err => console.log(err)); + + return successfullResponse; +}; + +module.exports.deleteListener = async () => { + await ddb.delete({ TableName:'listeners', Key:{ name:'default' } }).promise(); + + return successfullResponse; +}; + +const newAWSApiGatewayManagementApi = event => { + let endpoint = event.apiGatewayUrl; + + if (!endpoint) endpoint = `${event.requestContext.domainName}/${event.requestContext.stage}`; + const apiVersion = '2018-11-29'; + + return new AWS.ApiGatewayManagementApi({ apiVersion, endpoint }); +}; + +const sendToClient = (data, connectionId, apigwManagementApi) => { + // console.log(`sendToClient:${connectionId}`); + let sendee = data; + if (typeof data === 'object') sendee = JSON.stringify(data); + + return apigwManagementApi.postToConnection({ ConnectionId: connectionId, Data: sendee }).promise(); +}; diff --git a/manual_test_websocket/manual_test_websocket_main/package-lock.json b/manual_test_websocket/main/package-lock.json similarity index 100% rename from manual_test_websocket/manual_test_websocket_main/package-lock.json rename to manual_test_websocket/main/package-lock.json diff --git a/manual_test_websocket/manual_test_websocket_main/package.json b/manual_test_websocket/main/package.json similarity index 100% rename from manual_test_websocket/manual_test_websocket_main/package.json rename to manual_test_websocket/main/package.json diff --git a/manual_test_websocket/manual_test_websocket_main/scripts/deploy_to_aws.sh b/manual_test_websocket/main/scripts/deploy_to_aws.sh similarity index 100% rename from manual_test_websocket/manual_test_websocket_main/scripts/deploy_to_aws.sh rename to manual_test_websocket/main/scripts/deploy_to_aws.sh diff --git a/manual_test_websocket/manual_test_websocket_main/scripts/deploy_to_offline.sh b/manual_test_websocket/main/scripts/deploy_to_offline.sh similarity index 100% rename from manual_test_websocket/manual_test_websocket_main/scripts/deploy_to_offline.sh rename to manual_test_websocket/main/scripts/deploy_to_offline.sh diff --git a/manual_test_websocket/manual_test_websocket_main/scripts/serverless..yml b/manual_test_websocket/main/scripts/serverless..yml similarity index 100% rename from manual_test_websocket/manual_test_websocket_main/scripts/serverless..yml rename to manual_test_websocket/main/scripts/serverless..yml diff --git a/manual_test_websocket/manual_test_websocket_main/scripts/serverless.aws.yml b/manual_test_websocket/main/scripts/serverless.aws.yml similarity index 100% rename from manual_test_websocket/manual_test_websocket_main/scripts/serverless.aws.yml rename to manual_test_websocket/main/scripts/serverless.aws.yml diff --git a/manual_test_websocket/manual_test_websocket_main/scripts/serverless.offline.yml b/manual_test_websocket/main/scripts/serverless.offline.yml similarity index 100% rename from manual_test_websocket/manual_test_websocket_main/scripts/serverless.offline.yml rename to manual_test_websocket/main/scripts/serverless.offline.yml diff --git a/manual_test_websocket/manual_test_websocket_main/serverless.yml b/manual_test_websocket/main/serverless.yml similarity index 100% rename from manual_test_websocket/manual_test_websocket_main/serverless.yml rename to manual_test_websocket/main/serverless.yml diff --git a/manual_test_websocket/main/test/e2e/ws.e2e.js b/manual_test_websocket/main/test/e2e/ws.e2e.js new file mode 100644 index 000000000..60f8a3706 --- /dev/null +++ b/manual_test_websocket/main/test/e2e/ws.e2e.js @@ -0,0 +1,360 @@ +/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable no-unused-expressions */ +const chai = require('chai'); +const chaiHttp = require('chai-http'); + +chai.use(chaiHttp); +const expect = chai.expect; +const aws4 = require('aws4'); +const awscred = require('awscred'); +const moment = require('moment'); + +const endpoint = process.env.npm_config_endpoint || 'ws://localhost:3001'; +const timeout = process.env.npm_config_timeout ? parseInt(process.env.npm_config_timeout) : 1000; +const WebSocketTester = require('../support/WebSocketTester'); + +describe('serverless', () => { + describe('with WebSocket support', () => { + let clients = []; let req = null; let cred = null; + const createWebSocket = async qs => { + const ws = new WebSocketTester(); + let url = endpoint; + if (qs) url = `${endpoint}?${qs}`; + await ws.open(url); + clients.push(ws); + + return ws; + }; + const createClient = async qs => { + const ws = await createWebSocket(qs); + ws.send(JSON.stringify({ action:'getClientInfo' })); + const json = await ws.receive1(); + const id = JSON.parse(json).info.id; + + return { ws, id }; + }; + before(async () => { + req = chai.request(`${endpoint.replace('ws://', 'http://').replace('wss://', 'https://')}`).keepOpen(); + // req=chai.request('http://localhost:3001/dev').keepOpen(); + cred = await new Promise((resolve, reject) => { + awscred.loadCredentials((err, data) => { + if (err) reject(err); else resolve(data); + }); + }); + }); + + beforeEach(() => { + clients = []; + }); + afterEach(async () => { + await Promise.all(clients.map(async (ws, i) => { + const n = ws.countUnrecived(); + + if (n > 0) { + console.log(`unreceived:[i=${i}]`); + (await ws.receive(n)).forEach(m => console.log(m)); + } + + expect(n).to.equal(0); + ws.close(); + })); + clients = []; + }); + + it('should request to upgade to WebSocket when receving an HTTP request', async () => { + const req = chai.request(`${endpoint.replace('ws://', 'http://').replace('wss://', 'https://')}`).keepOpen(); + let res = await req.get(`/${Date.now()}`);// .set('Authorization', user.accessToken); + + expect(res).to.have.status(426); + + res = await req.get(`/${Date.now()}/${Date.now()}`);// .set('Authorization', user.accessToken); + + expect(res).to.have.status(426); + }).timeout(timeout); + + it('should open a WebSocket', async () => { + const ws = await createWebSocket(); + expect(ws).not.to.be.undefined; + }).timeout(timeout); + + it('should receive client connection info', async () => { + const ws = await createWebSocket(); + ws.send(JSON.stringify({ action:'getClientInfo' })); + const clientInfo = JSON.parse(await ws.receive1()); + + expect(clientInfo).to.deep.equal({ action:'update', event:'client-info', info:{ id:clientInfo.info.id } }); + }).timeout(timeout); + + it('should call default handler when no such action exists', async () => { + const ws = await createWebSocket(); + const payload = JSON.stringify({ action:`action${Date.now()}` }); + ws.send(payload); + + expect(await ws.receive1()).to.equal(`Error: No Supported Action in Payload '${payload}'`); + }).timeout(timeout); + + it('should call default handler when no action provided', async () => { + const ws = await createWebSocket(); + ws.send(JSON.stringify({ hello:'world' })); + + expect(await ws.receive1()).to.equal('Error: No Supported Action in Payload \'{"hello":"world"}\''); + }).timeout(timeout); + + it('should send & receive data', async () => { + const c1 = await createClient(); + const c2 = await createClient(); + c1.ws.send(JSON.stringify({ action:'send', data:'Hello World!', clients:[c1.id, c2.id] })); + + expect(await c1.ws.receive1()).to.equal('Hello World!'); + expect(await c2.ws.receive1()).to.equal('Hello World!'); + }).timeout(timeout); + + it('should respond when having an internal server error', async () => { + const conn = await createClient(); + conn.ws.send(JSON.stringify({ action:'makeError' })); + const res = JSON.parse(await conn.ws.receive1()); + + expect(res).to.deep.equal({ message:'Internal server error', connectionId:conn.id, requestId:res.requestId }); + }).timeout(timeout); + + it('should respond via callback', async () => { + const ws = await createWebSocket(); + ws.send(JSON.stringify({ action:'replyViaCallback' })); + const res = JSON.parse(await ws.receive1()); + expect(res).to.deep.equal({ action:'update', event:'reply-via-callback' }); + }).timeout(timeout); + + it('should respond with error when calling callback(error)', async () => { + const conn = await createClient(); + conn.ws.send(JSON.stringify({ action:'replyErrorViaCallback' })); + const res = JSON.parse(await conn.ws.receive1()); + expect(res).to.deep.equal({ message:'Internal server error', connectionId:conn.id, requestId:res.requestId }); + }).timeout(timeout); + + it('should respond with only the last action when there are more than one in the serverless.yml file', async () => { + const ws = await createWebSocket(); + ws.send(JSON.stringify({ action:'makeMultiCalls' })); + const res = JSON.parse(await ws.receive1()); + + expect(res).to.deep.equal({ action:'update', event:'made-call-2' }); + }).timeout(timeout); + + it('should not send to non existing client', async () => { + const c1 = await createClient(); + c1.ws.send(JSON.stringify({ action:'send', data:'Hello World!', clients:['non-existing-id'] })); + + expect(await c1.ws.receive1()).to.equal('Error: Could not Send all Messages'); + }).timeout(timeout); + + it('should connect & disconnect', async () => { + const ws = await createWebSocket(); + await ws.send(JSON.stringify({ action:'registerListener' })); + await ws.receive1(); + + const c1 = await createClient(); + const connect1 = JSON.parse(await ws.receive1()); delete connect1.info.event; delete delete connect1.info.context; + expect(connect1).to.deep.equal({ action:'update', event:'connect', info:{ id:c1.id } }); + + const c2 = await createClient(); + const connect2 = JSON.parse(await ws.receive1()); delete connect2.info.event; delete delete connect2.info.context; + expect(connect2).to.deep.equal({ action:'update', event:'connect', info:{ id:c2.id } }); + + c2.ws.close(); + const disconnect2 = JSON.parse(await ws.receive1()); delete disconnect2.info.event; delete delete disconnect2.info.context; + expect(disconnect2).to.deep.equal({ action:'update', event:'disconnect', info:{ id:c2.id } }); + + const c3 = await createClient(); + const connect3 = JSON.parse(await ws.receive1()); delete connect3.info.event; delete delete connect3.info.context; + expect(connect3).to.deep.equal({ action:'update', event:'connect', info:{ id:c3.id } }); + + c1.ws.close(); + const disconnect1 = JSON.parse(await ws.receive1()); delete disconnect1.info.event; delete delete disconnect1.info.context; + expect(disconnect1).to.deep.equal({ action:'update', event:'disconnect', info:{ id:c1.id } }); + + c3.ws.close(); + const disconnect3 = JSON.parse(await ws.receive1()); delete disconnect3.info.event; delete delete disconnect3.info.context; + expect(disconnect3).to.deep.equal({ action:'update', event:'disconnect', info:{ id:c3.id } }); + }).timeout(timeout); + + const createExpectedEvent = (connectionId, action, eventType, actualEvent) => { + const url = new URL(endpoint); + const expected = { + apiGatewayUrl: `${actualEvent.apiGatewayUrl}`, + isBase64Encoded: false, + requestContext: { + apiId: actualEvent.requestContext.apiId, + connectedAt: actualEvent.requestContext.connectedAt, + connectionId: `${connectionId}`, + domainName: url.hostname, + eventType, + extendedRequestId: actualEvent.requestContext.extendedRequestId, + identity: { + accessKey: null, + accountId: null, + caller: null, + cognitoAuthenticationProvider: null, + cognitoAuthenticationType: null, + cognitoIdentityId: null, + cognitoIdentityPoolId: null, + principalOrgId: null, + sourceIp: actualEvent.requestContext.identity.sourceIp, + user: null, + userAgent: null, + userArn: null, + }, + messageDirection: 'IN', + messageId: actualEvent.requestContext.messageId, + requestId: actualEvent.requestContext.requestId, + requestTime: actualEvent.requestContext.requestTime, + requestTimeEpoch: actualEvent.requestContext.requestTimeEpoch, + routeKey: action, + stage: actualEvent.requestContext.stage, + }, + }; + + return expected; + }; + + const createExpectedContext = actualContext => { + const expected = { + awsRequestId: actualContext.awsRequestId, + callbackWaitsForEmptyEventLoop: true, + functionName: actualContext.functionName, + functionVersion: '$LATEST', + invokedFunctionArn: actualContext.invokedFunctionArn, + invokeid: actualContext.invokeid, + logGroupName: actualContext.logGroupName, + logStreamName: actualContext.logStreamName, + memoryLimitInMB: actualContext.memoryLimitInMB, + }; + + return expected; + }; + + const createExpectedConnectHeaders = actualHeaders => { + const url = new URL(endpoint); + const expected = { + Host: url.hostname, + 'Sec-WebSocket-Extensions': actualHeaders['Sec-WebSocket-Extensions'], + 'Sec-WebSocket-Key': actualHeaders['Sec-WebSocket-Key'], + 'Sec-WebSocket-Version': actualHeaders['Sec-WebSocket-Version'], + 'X-Amzn-Trace-Id': actualHeaders['X-Amzn-Trace-Id'], + 'X-Forwarded-For': actualHeaders['X-Forwarded-For'], + 'X-Forwarded-Port': `${url.port || 443}`, + 'X-Forwarded-Proto': `${url.protocol.replace('ws', 'http').replace('wss', 'https').replace(':', '')}`, + }; + + return expected; + }; + + const createExpectedDisconnectHeaders = () => { + const url = new URL(endpoint); + const expected = { + Host: url.hostname, + 'x-api-key': '', + 'x-restapi': '', + }; + + return expected; + }; + + const createExpectedConnectMultiValueHeaders = actualHeaders => { + const expected = createExpectedConnectHeaders(actualHeaders); + Object.keys(expected).forEach(key => { + expected[key] = [expected[key]]; + }); + + return expected; + }; + + const createExpectedDisconnectMultiValueHeaders = actualHeaders => { + const expected = createExpectedDisconnectHeaders(actualHeaders); + Object.keys(expected).forEach(key => { + expected[key] = [expected[key]]; + }); + + return expected; + }; + + it('should receive correct call info', async () => { + const ws = await createWebSocket(); + await ws.send(JSON.stringify({ action:'registerListener' })); + await ws.receive1(); + + // connect + const c = await createClient(); + const connect = JSON.parse(await ws.receive1()); + let now = Date.now(); + let expectedCallInfo = { id:c.id, event:{ headers:createExpectedConnectHeaders(connect.info.event.headers), multiValueHeaders:createExpectedConnectMultiValueHeaders(connect.info.event.headers), ...createExpectedEvent(c.id, '$connect', 'CONNECT', connect.info.event) }, context:createExpectedContext(connect.info.context) }; + + expect(connect).to.deep.equal({ action:'update', event:'connect', info:expectedCallInfo }); + expect(connect.info.event.requestContext.requestTimeEpoch).to.be.within(connect.info.event.requestContext.connectedAt - 10, connect.info.event.requestContext.requestTimeEpoch + 10); + expect(connect.info.event.requestContext.connectedAt).to.be.within(now - timeout, now); + expect(connect.info.event.requestContext.requestTimeEpoch).to.be.within(now - timeout, now); + expect(moment.utc(connect.info.event.requestContext.requestTime, 'D/MMM/YYYY:H:m:s Z').toDate().getTime()).to.be.within(now - timeout, now); + + if (endpoint.startsWith('ws://locahost')) { + expect(connect.info.event.apiGatewayUrl).to.equal(endpoint.replace('ws://', 'http://').replace('wss://', 'https://')); + expect(connect.info.event.headers['X-Forwarded-For']).to.be.equal('127.0.0.1'); + } + + // getCallInfo + c.ws.send(JSON.stringify({ action:'getCallInfo' })); + const callInfo = JSON.parse(await c.ws.receive1()); + now = Date.now(); + expectedCallInfo = { event:{ body: '{"action":"getCallInfo"}', ...createExpectedEvent(c.id, 'getCallInfo', 'MESSAGE', callInfo.info.event) }, context:createExpectedContext(callInfo.info.context) }; + + expect(callInfo).to.deep.equal({ action:'update', event:'call-info', info:expectedCallInfo }); + expect(callInfo.info.event.requestContext.connectedAt).to.be.lt(callInfo.info.event.requestContext.requestTimeEpoch); + expect(callInfo.info.event.requestContext.connectedAt).to.be.within(now - timeout, now); + expect(callInfo.info.event.requestContext.requestTimeEpoch).to.be.within(now - timeout, now); + expect(moment.utc(callInfo.info.event.requestContext.requestTime, 'D/MMM/YYYY:H:m:s Z').toDate().getTime()).to.be.within(now - timeout, now); + if (endpoint.startsWith('ws://locahost')) expect(callInfo.info.event.apiGatewayUrl).to.equal(endpoint.replace('ws://', 'http://').replace('wss://', 'https://')); + + // disconnect + c.ws.close(); + const disconnect = JSON.parse(await ws.receive1()); + now = Date.now(); + expectedCallInfo = { id:c.id, event:{ headers:createExpectedDisconnectHeaders(disconnect.info.event.headers), multiValueHeaders:createExpectedDisconnectMultiValueHeaders(disconnect.info.event.headers), ...createExpectedEvent(c.id, '$disconnect', 'DISCONNECT', disconnect.info.event) }, context:createExpectedContext(disconnect.info.context) }; + + expect(disconnect).to.deep.equal({ action:'update', event:'disconnect', info:expectedCallInfo }); + }).timeout(timeout); + + it('should be able to parse query string', async () => { + const now = `${Date.now()}`; + const ws = await createWebSocket(); + await ws.send(JSON.stringify({ action:'registerListener' })); + await ws.receive1(); + + await createClient(); + await createClient(`now=${now}&before=123456789`); + + expect(JSON.parse(await ws.receive1()).info.event.queryStringParameters).to.be.undefined; + expect(JSON.parse(await ws.receive1()).info.event.queryStringParameters).to.deep.equal({ now, before:'123456789' }); + }).timeout(timeout); + + it('should be able to receive messages via REST API', async () => { + await createClient(); + const c2 = await createClient(); + const url = new URL(endpoint); + const signature = { service: 'execute-api', host:url.host, path:`${url.pathname}/@connections/${c2.id}`, method: 'POST', body:'Hello World!', headers:{ 'Content-Type':'text/plain'/* 'application/text' */ } }; + aws4.sign(signature, { accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey }); + const res = await req.post(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers.Authorization).set('Content-Type', signature.headers['Content-Type']) +.send('Hello World!'); + + expect(res).to.have.status(200); + expect(await c2.ws.receive1()).to.equal('Hello World!'); + }).timeout(timeout); + + it('should receive error code when sending to non existing client via REST API', async () => { + const c = 'aJz0Md6VoAMCIbQ='; + const url = new URL(endpoint); + const signature = { service: 'execute-api', host:url.host, path:`${url.pathname}/@connections/${c}`, method: 'POST', body:'Hello World!', headers:{ 'Content-Type':'text/plain'/* 'application/text' */ } }; + aws4.sign(signature, { accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey }); + const res = await req.post(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers.Authorization).set('Content-Type', signature.headers['Content-Type']) +.send('Hello World!'); + + expect(res).to.have.status(410); + }).timeout(timeout); + }); +}); diff --git a/manual_test_websocket/main/test/support/WebSocketTester.js b/manual_test_websocket/main/test/support/WebSocketTester.js new file mode 100644 index 000000000..aaeff5a4a --- /dev/null +++ b/manual_test_websocket/main/test/support/WebSocketTester.js @@ -0,0 +1,62 @@ +/* eslint-disable import/no-extraneous-dependencies */ +const WebSocket = require('ws'); + +class WebSocketTester { + constructor() { + this.messages = []; this.receivers = []; + } + + open(url) { + if (this.ws != null) return; + const ws = this.ws = new WebSocket(url); + ws.on('message', message => { + // console.log('Received: '+message); + if (this.receivers.length > 0) this.receivers.shift()(message); + else this.messages.push(message); + }); + + return new Promise(resolve => { + ws.on('open', () => { + resolve(true); + }); + }); + } + + send(data) { + this.ws.send(data); + } + + receive1() { + return new Promise(resolve => { + if (this.messages.length > 0) resolve(this.messages.shift()); + else this.receivers.push(resolve); + }); + } + + receive(n) { + return new Promise(resolve => { + const messages = []; + for (let i = 0; i < n; i += 1) { + this.receive1().then(message => { + messages[i] = message; + if (i === n - 1) resolve(messages); + }); + } + }); + } + + skip() { + if (this.messages.length > 0) this.messages.shift(); + else this.receivers.push(() => {}); + } + + countUnrecived() { + return this.messages.length; + } + + close() { + if (this.ws != null) this.ws.close(); + } +} + +module.exports = WebSocketTester; diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/handler.js b/manual_test_websocket/manual_test_websocket_RouteSelection/handler.js deleted file mode 100644 index e8da78fd7..000000000 --- a/manual_test_websocket/manual_test_websocket_RouteSelection/handler.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -const AWS = require('aws-sdk'); - - -const successfullResponse = { - statusCode: 200, - body: 'Request is OK.' -}; - -const errorResponse = { - statusCode: 400, - body: 'Request is not OK.' -}; - -module.exports.echo = async (event, context) => { - const action = JSON.parse(event.body); - - await sendToClient(action.message, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)); - return successfullResponse; -}; - -const newAWSApiGatewayManagementApi=(event, context)=>{ - let endpoint=event.apiGatewayUrl; - - if (!endpoint) endpoint = event.requestContext.domainName+'/'+event.requestContext.stage; - const apiVersion='2018-11-29'; - return new AWS.ApiGatewayManagementApi({ apiVersion, endpoint }); -}; - -const sendToClient = (data, connectionId, apigwManagementApi) => { - // console.log(`sendToClient:${connectionId}`); - let sendee=data; - if ('object'==typeof data) sendee=JSON.stringify(data); - - return apigwManagementApi.postToConnection({ConnectionId: connectionId, Data: sendee}).promise(); -}; diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/test/e2e/ws.e2e.js b/manual_test_websocket/manual_test_websocket_RouteSelection/test/e2e/ws.e2e.js deleted file mode 100644 index a5659364d..000000000 --- a/manual_test_websocket/manual_test_websocket_RouteSelection/test/e2e/ws.e2e.js +++ /dev/null @@ -1,53 +0,0 @@ -const chai = require('chai'); -const expect = chai.expect; -const endpoint = process.env.npm_config_endpoint||'ws://localhost:3005'; -const timeout = process.env.npm_config_timeout?parseInt(process.env.npm_config_timeout):1000; -const WebSocketTester=require('../support/WebSocketTester'); - -describe('serverless', ()=>{ - describe('with WebSocket support', ()=>{ - let clients=[]; let req=null; let cred=null; - const createWebSocket=async (qs)=>{ - const ws=new WebSocketTester(); - let url=endpoint; - if (qs) url=`${endpoint}?${qs}`; - await ws.open(url); - clients.push(ws); - return ws; - }; - const createClient=async (qs)=>{ - const ws=await createWebSocket(qs); - ws.send(JSON.stringify({action:'getClientInfo'})); - const json=await ws.receive1(); - const id=JSON.parse(json).info.id; - return {ws, id}; - }; - - beforeEach(()=>{ - clients=[]; - }); - afterEach(async ()=>{ - await Promise.all(clients.map(async (ws, i)=>{ - const n=ws.countUnrecived(); - - if (n>0) { - console.log(`unreceived:[i=${i}]`); - (await ws.receive(n)).forEach(m=>console.log(m)); - } - expect(n).to.equal(0); - ws.close(); - })); - clients=[]; - }); - - it(`should call action 'echo' handler located at service.do`, async ()=>{ - const ws=await createWebSocket(); - const now=""+Date.now(); - const payload=JSON.stringify({service:{do:'echo'}, message:now}); - ws.send(payload); - expect(await ws.receive1()).to.equal(`${now}`); - }).timeout(timeout); - - - }); -}); \ No newline at end of file diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/test/support/WebSocketTester.js b/manual_test_websocket/manual_test_websocket_RouteSelection/test/support/WebSocketTester.js deleted file mode 100644 index d5e80057a..000000000 --- a/manual_test_websocket/manual_test_websocket_RouteSelection/test/support/WebSocketTester.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; - -const WebSocket = require('ws'); - -class WebSocketTester { - constructor() { - this.messages=[]; this.receivers=[]; - } - - open(url) { - if (null!=this.ws) return; - const ws=this.ws=new WebSocket(url); - ws.on('message', (message)=>{ - // console.log('Received: '+message); - if (0 { - ws.on('open', ()=>{ - resolve(true); - }); - }); - } - - send(data) { - this.ws.send(data); - } - - receive1() { - return new Promise((resolve/*, reject*/)=>{ - if (0{ - const messages=[]; - for (let i=0; i{ - messages[i]=message; - if (i===n-1) resolve(messages); - }); - } - }); - } - - skip() { - if (0{}); - } - - countUnrecived() { - return this.messages.length; - } - - close() { - if (null!=this.ws) this.ws.close(); - } -}; - -module.exports=WebSocketTester; diff --git a/manual_test_websocket/manual_test_websocket_main/handler.js b/manual_test_websocket/manual_test_websocket_main/handler.js deleted file mode 100644 index 7ada0f080..000000000 --- a/manual_test_websocket/manual_test_websocket_main/handler.js +++ /dev/null @@ -1,150 +0,0 @@ -'use strict'; - -const AWS = require('aws-sdk'); -const ddb = (()=>{ - if (process.env.IS_OFFLINE) return new AWS.DynamoDB.DocumentClient({region: "localhost", endpoint: "http://localhost:8000"}); - return new AWS.DynamoDB.DocumentClient(); -})(); - - -const successfullResponse = { - statusCode: 200, - body: 'Request is OK.' -}; - -const errorResponse = { - statusCode: 400, - body: 'Request is not OK.' -}; - -// const generatePolicy = function(principalId, effect, resource) { -// const authResponse = {}; -// authResponse.principalId = principalId; -// if (effect && resource) { -// const policyDocument = {}; -// policyDocument.Version = '2012-10-17'; -// policyDocument.Statement = []; -// const statementOne = {}; -// statementOne.Action = 'execute-api:Invoke'; -// statementOne.Effect = effect; -// statementOne.Resource = resource; -// policyDocument.Statement[0] = statementOne; -// authResponse.policyDocument = policyDocument; -// } -// return authResponse; -// }; - -// module.exports.http = async (event, context) => { -// return successfullResponse; -// }; - -module.exports.connect = async (event, context) => { - // console.log('connect:'); - const listener=await ddb.get({TableName:'listeners', Key:{name:'default'}}).promise(); - - if (listener.Item) { - const timeout=new Promise((resolve) => setTimeout(resolve,100)); - const send=sendToClient( // sendToClient won't return on AWS when client doesn't exits so we set a timeout - JSON.stringify({action:'update', event:'connect', info:{id:event.requestContext.connectionId, event:{...event, apiGatewayUrl:`${event.apiGatewayUrl}`}, context}}), - listener.Item.id, - newAWSApiGatewayManagementApi(event, context)).catch(()=>{}); - await Promise.race([send, timeout]); - } - return successfullResponse; -}; - -// module.export.auth = (event, context, callback) => { -// //console.log('auth:'); -// const token = event.headers["Authorization"]; - -// if ('deny'===token) callback(null, generatePolicy('user', 'Deny', event.methodArn)); -// else callback(null, generatePolicy('user', 'Allow', event.methodArn));; -// }; - -module.exports.disconnect = async (event, context) => { - const listener=await ddb.get({TableName:'listeners', Key:{name:'default'}}).promise(); - if (listener.Item) await sendToClient(JSON.stringify({action:'update', event:'disconnect', info:{id:event.requestContext.connectionId, event:{...event, apiGatewayUrl:`${event.apiGatewayUrl}`}, context}}), listener.Item.id, newAWSApiGatewayManagementApi(event, context)).catch(()=>{}); - return successfullResponse; -}; - -module.exports.defaultHandler = async (event, context) => { - await sendToClient(`Error: No Supported Action in Payload '${event.body}'`, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err=>console.log(err)); - return successfullResponse; -}; - -module.exports.getClientInfo = async (event, context) => { - // console.log('getClientInfo:'); - await sendToClient({action:'update', event:'client-info', info:{id:event.requestContext.connectionId}}, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err=>console.log(err)); - return successfullResponse; -}; - -module.exports.getCallInfo = async (event, context) => { - await sendToClient({action:'update', event:'call-info', info:{event:{...event, apiGatewayUrl:`${event.apiGatewayUrl}`}, context}}, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err=>console.log(err)); - return successfullResponse; -}; - -module.exports.makeError = async (event, context) => { - const obj=null; - obj.non.non=1; - return successfullResponse; -}; - -module.exports.replyViaCallback = (event, context, callback) => { - sendToClient({action:'update', event:'reply-via-callback'}, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err=>console.log(err)); - callback(); -}; - -module.exports.replyErrorViaCallback = (event, context, callback) => { - return callback("error error error"); -}; - -module.exports.multiCall1 = async (event, context) => { - await sendToClient({action:'update', event:'made-call-1'}, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err=>console.log(err)); - return successfullResponse; -}; - -module.exports.multiCall2 = async (event, context) => { - await sendToClient({action:'update', event:'made-call-2'}, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err=>console.log(err)); - return successfullResponse; -}; - - -module.exports.send = async (event, context) => { - const action = JSON.parse(event.body); - const sents=[]; - action.clients.forEach((connectionId)=>{ - const sent=sendToClient(action.data, connectionId, newAWSApiGatewayManagementApi(event, context)); - sents.push(sent); - }); - const noErr=await Promise.all(sents).then(()=>true).catch(()=>false); - if (!noErr) await sendToClient('Error: Could not Send all Messages', event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)); - return successfullResponse; -}; - -module.exports.registerListener = async (event, context) => { - await ddb.put({TableName:'listeners', Item:{name:'default', id:event.requestContext.connectionId}}).promise(); - await sendToClient({action:'update', event:'register-listener', info:{id:event.requestContext.connectionId}}, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err=>console.log(err)); - return successfullResponse; -}; - -module.exports.deleteListener = async (event, context) => { - await ddb.delete({TableName:'listeners', Key:{name:'default'}}).promise(); - - return successfullResponse; -}; - -const newAWSApiGatewayManagementApi=(event, context)=>{ - let endpoint=event.apiGatewayUrl; - - if (!endpoint) endpoint = event.requestContext.domainName+'/'+event.requestContext.stage; - const apiVersion='2018-11-29'; - return new AWS.ApiGatewayManagementApi({ apiVersion, endpoint }); -}; - -const sendToClient = (data, connectionId, apigwManagementApi) => { - // console.log(`sendToClient:${connectionId}`); - let sendee=data; - if ('object'==typeof data) sendee=JSON.stringify(data); - - return apigwManagementApi.postToConnection({ConnectionId: connectionId, Data: sendee}).promise(); -}; diff --git a/manual_test_websocket/manual_test_websocket_main/test/e2e/ws.e2e.js b/manual_test_websocket/manual_test_websocket_main/test/e2e/ws.e2e.js deleted file mode 100644 index d8db0b706..000000000 --- a/manual_test_websocket/manual_test_websocket_main/test/e2e/ws.e2e.js +++ /dev/null @@ -1,342 +0,0 @@ -const chai = require('chai'); -const chaiHttp = require('chai-http'); -chai.use(chaiHttp); -const expect = chai.expect; -const aws4 = require('aws4'); -const awscred = require('awscred'); -const moment = require('moment'); -const endpoint = process.env.npm_config_endpoint||'ws://localhost:3001'; -const timeout = process.env.npm_config_timeout?parseInt(process.env.npm_config_timeout):1000; -const WebSocketTester=require('../support/WebSocketTester'); - -describe('serverless', ()=>{ - describe('with WebSocket support', ()=>{ - let clients=[]; let req=null; let cred=null; - const createWebSocket=async (qs)=>{ - const ws=new WebSocketTester(); - let url=endpoint; - if (qs) url=`${endpoint}?${qs}`; - await ws.open(url); - clients.push(ws); - return ws; - }; - const createClient=async (qs)=>{ - const ws=await createWebSocket(qs); - ws.send(JSON.stringify({action:'getClientInfo'})); - const json=await ws.receive1(); - const id=JSON.parse(json).info.id; - return {ws, id}; - }; - before(async ()=>{ - req=chai.request(`${endpoint.replace('ws://', 'http://').replace('wss://', 'https://')}`).keepOpen(); - // req=chai.request('http://localhost:3001/dev').keepOpen(); - cred=await new Promise((resolve, reject)=>{ - awscred.loadCredentials(function(err, data) { if (err) reject(err); else resolve(data); }); - }); - }); - - beforeEach(()=>{ - clients=[]; - }); - afterEach(async ()=>{ - await Promise.all(clients.map(async (ws, i)=>{ - const n=ws.countUnrecived(); - - if (n>0) { - console.log(`unreceived:[i=${i}]`); - (await ws.receive(n)).forEach(m=>console.log(m)); - } - expect(n).to.equal(0); - ws.close(); - })); - clients=[]; - }); - - it('should request to upgade to WebSocket when receving an HTTP request', async ()=>{ - const req=chai.request(`${endpoint.replace('ws://', 'http://').replace('wss://', 'https://')}`).keepOpen(); - let res=await req.get(`/${Date.now()}`);//.set('Authorization', user.accessToken); - expect(res).to.have.status(426); - res=await req.get(`/${Date.now()}/${Date.now()}`);//.set('Authorization', user.accessToken); - expect(res).to.have.status(426); - }).timeout(timeout); - - it('should open a WebSocket', async ()=>{ - const ws=await createWebSocket(); - expect(ws).not.to.be.undefined; - }).timeout(timeout); - - it('should receive client connection info', async ()=>{ - const ws=await createWebSocket(); - ws.send(JSON.stringify({action:'getClientInfo'})); - const clientInfo=JSON.parse(await ws.receive1()); - expect(clientInfo).to.deep.equal({action:'update', event:'client-info', info:{id:clientInfo.info.id}}); - }).timeout(timeout); - - it('should call default handler when no such action exists', async ()=>{ - const ws=await createWebSocket(); - const payload=JSON.stringify({action:'action'+Date.now()}); - ws.send(payload); - expect(await ws.receive1()).to.equal(`Error: No Supported Action in Payload '${payload}'`); - }).timeout(timeout); - - it('should call default handler when no action provided', async ()=>{ - const ws=await createWebSocket(); - ws.send(JSON.stringify({hello:'world'})); - expect(await ws.receive1()).to.equal(`Error: No Supported Action in Payload '{"hello":"world"}'`); - }).timeout(timeout); - - it('should send & receive data', async ()=>{ - const c1=await createClient(); - const c2=await createClient(); - const c3=await createClient(); - c1.ws.send(JSON.stringify({action:'send', data:'Hello World!', clients:[c1.id, c3.id]})); - expect(await c1.ws.receive1()).to.equal('Hello World!'); - expect(await c3.ws.receive1()).to.equal('Hello World!'); - }).timeout(timeout); - - it('should respond when having an internal server error', async ()=>{ - const conn=await createClient(); - conn.ws.send(JSON.stringify({action:'makeError'})); - const res=JSON.parse(await conn.ws.receive1()); - expect(res).to.deep.equal({message:'Internal server error', connectionId:conn.id, requestId:res.requestId}); - }).timeout(timeout); - - it('should respond via callback', async ()=>{ - const ws=await createWebSocket(); - ws.send(JSON.stringify({action:'replyViaCallback'})); - const res=JSON.parse(await ws.receive1()); - expect(res).to.deep.equal({action:'update', event:'reply-via-callback'}); - }).timeout(timeout); - - it('should respond with error when calling callback(error)', async ()=>{ - const conn=await createClient(); - conn.ws.send(JSON.stringify({action:'replyErrorViaCallback'})); - const res=JSON.parse(await conn.ws.receive1()); - expect(res).to.deep.equal({message:'Internal server error', connectionId:conn.id, requestId:res.requestId}); - }).timeout(timeout); - - it('should respond with only the last action when there are more than one in the serverless.yml file', async ()=>{ - const ws=await createWebSocket(); - ws.send(JSON.stringify({action:'makeMultiCalls'})); - const res=JSON.parse(await ws.receive1()); - expect(res).to.deep.equal({action:'update', event:'made-call-2'}); - }).timeout(timeout); - - it('should not send to non existing client', async ()=>{ - const c1=await createClient(); - c1.ws.send(JSON.stringify({action:'send', data:'Hello World!', clients:["non-existing-id"]})); - expect(await c1.ws.receive1()).to.equal('Error: Could not Send all Messages'); - }).timeout(timeout); - - it('should connect & disconnect', async ()=>{ - const ws=await createWebSocket(); - await ws.send(JSON.stringify({action:'registerListener'})); - await ws.receive1(); - - const c1=await createClient(); - const connect1 = JSON.parse(await ws.receive1()); delete connect1.info.event; delete delete connect1.info.context; - expect(connect1).to.deep.equal({action:'update', event:'connect', info:{id:c1.id}}); - - const c2=await createClient(); - const connect2 = JSON.parse(await ws.receive1()); delete connect2.info.event; delete delete connect2.info.context; - expect(connect2).to.deep.equal({action:'update', event:'connect', info:{id:c2.id}}); - - c2.ws.close(); - const disconnect2 = JSON.parse(await ws.receive1()); delete disconnect2.info.event; delete delete disconnect2.info.context; - expect(disconnect2).to.deep.equal({action:'update', event:'disconnect', info:{id:c2.id}}); - - const c3=await createClient(); - const connect3 = JSON.parse(await ws.receive1()); delete connect3.info.event; delete delete connect3.info.context; - expect(connect3).to.deep.equal({action:'update', event:'connect', info:{id:c3.id}}); - - c1.ws.close(); - const disconnect1 = JSON.parse(await ws.receive1()); delete disconnect1.info.event; delete delete disconnect1.info.context; - expect(disconnect1).to.deep.equal({action:'update', event:'disconnect', info:{id:c1.id}}); - - c3.ws.close(); - const disconnect3 = JSON.parse(await ws.receive1()); delete disconnect3.info.event; delete delete disconnect3.info.context; - expect(disconnect3).to.deep.equal({action:'update', event:'disconnect', info:{id:c3.id}}); - }).timeout(timeout); - - const createExpectedEvent=(connectionId, action, eventType, actualEvent)=>{ - const url=new URL(endpoint); - const expected={ - apiGatewayUrl: `${actualEvent.apiGatewayUrl}`, - isBase64Encoded: false, - requestContext: { - apiId: actualEvent.requestContext.apiId, - connectedAt: actualEvent.requestContext.connectedAt, - connectionId: `${connectionId}`, - domainName: url.hostname, - eventType, - extendedRequestId: actualEvent.requestContext.extendedRequestId, - identity: { - accessKey: null, - accountId: null, - caller: null, - cognitoAuthenticationProvider: null, - cognitoAuthenticationType: null, - cognitoIdentityId: null, - cognitoIdentityPoolId: null, - principalOrgId: null, - sourceIp: actualEvent.requestContext.identity.sourceIp, - user: null, - userAgent: null, - userArn: null, - }, - messageDirection: 'IN', - messageId: actualEvent.requestContext.messageId, - requestId: actualEvent.requestContext.requestId, - requestTime: actualEvent.requestContext.requestTime, - requestTimeEpoch: actualEvent.requestContext.requestTimeEpoch, - routeKey: action, - stage: actualEvent.requestContext.stage, - }, - }; - - return expected; - }; - - const createExpectedContext=(actualContext)=>{ - const expected={ - awsRequestId: actualContext.awsRequestId, - callbackWaitsForEmptyEventLoop: true, - functionName: actualContext.functionName, - functionVersion: '$LATEST', - invokedFunctionArn: actualContext.invokedFunctionArn, - invokeid: actualContext.invokeid, - logGroupName: actualContext.logGroupName, - logStreamName: actualContext.logStreamName, - memoryLimitInMB: actualContext.memoryLimitInMB, - }; - - return expected; - }; - - const createExpectedConnectHeaders=(actualHeaders)=>{ - const url=new URL(endpoint); - const expected={ - Host: url.hostname, - 'Sec-WebSocket-Extensions': actualHeaders['Sec-WebSocket-Extensions'], - 'Sec-WebSocket-Key': actualHeaders['Sec-WebSocket-Key'], - 'Sec-WebSocket-Version': actualHeaders['Sec-WebSocket-Version'], - 'X-Amzn-Trace-Id': actualHeaders['X-Amzn-Trace-Id'], - 'X-Forwarded-For': actualHeaders['X-Forwarded-For'], - 'X-Forwarded-Port': `${url.port||443}`, - 'X-Forwarded-Proto': `${url.protocol.replace('ws', 'http').replace('wss', 'https').replace(':', '')}` - }; - - return expected; - }; - - const createExpectedDisconnectHeaders=(actualHeaders)=>{ - const url=new URL(endpoint); - const expected={ - Host: url.hostname, - 'x-api-key': '', - 'x-restapi': '', - }; - - return expected; - }; - - const createExpectedConnectMultiValueHeaders=(actualHeaders)=>{ - const expected=createExpectedConnectHeaders(actualHeaders); - Object.keys(expected).map((key, index)=>{ - expected[key] = [expected[key]]; - }); - return expected; - }; - - const createExpectedDisconnectMultiValueHeaders=(actualHeaders)=>{ - const expected=createExpectedDisconnectHeaders(actualHeaders); - Object.keys(expected).map((key, index)=>{ - expected[key] = [expected[key]]; - }); - return expected; - }; - - it('should receive correct call info', async ()=>{ - const ws=await createWebSocket(); - await ws.send(JSON.stringify({action:'registerListener'})); - await ws.receive1(); - - // connect - const c=await createClient(); - const connect=JSON.parse(await ws.receive1()); - let now=Date.now(); - let expectedCallInfo={id:c.id, event:{headers:createExpectedConnectHeaders(connect.info.event.headers), multiValueHeaders:createExpectedConnectMultiValueHeaders(connect.info.event.headers), ...createExpectedEvent(c.id, '$connect', 'CONNECT', connect.info.event)}, context:createExpectedContext(connect.info.context)}; - expect(connect).to.deep.equal({action:'update', event:'connect', info:expectedCallInfo}); - expect(connect.info.event.requestContext.requestTimeEpoch).to.be.within(connect.info.event.requestContext.connectedAt-10, connect.info.event.requestContext.requestTimeEpoch+10); - expect(connect.info.event.requestContext.connectedAt).to.be.within(now-timeout, now); - expect(connect.info.event.requestContext.requestTimeEpoch).to.be.within(now-timeout, now); - expect(moment.utc(connect.info.event.requestContext.requestTime, 'D/MMM/YYYY:H:m:s Z').toDate().getTime()).to.be.within(now-timeout, now); - if (endpoint.startsWith('ws://locahost')) { - expect(connect.info.event.apiGatewayUrl).to.equal(endpoint.replace('ws://', 'http://').replace('wss://', 'https://')); - expect(connect.info.event.headers['X-Forwarded-For']).to.be.equal('127.0.0.1'); - } - - // getCallInfo - c.ws.send(JSON.stringify({action:'getCallInfo'})); - const callInfo=JSON.parse(await c.ws.receive1()); - now=Date.now(); - expectedCallInfo={event:{body: '{\"action\":\"getCallInfo\"}', ...createExpectedEvent(c.id, 'getCallInfo', 'MESSAGE', callInfo.info.event)}, context:createExpectedContext(callInfo.info.context)}; - expect(callInfo).to.deep.equal({action:'update', event:'call-info', info:expectedCallInfo}); - expect(callInfo.info.event.requestContext.connectedAt).to.be.lt(callInfo.info.event.requestContext.requestTimeEpoch); - expect(callInfo.info.event.requestContext.connectedAt).to.be.within(now-timeout, now); - expect(callInfo.info.event.requestContext.requestTimeEpoch).to.be.within(now-timeout, now); - expect(moment.utc(callInfo.info.event.requestContext.requestTime, 'D/MMM/YYYY:H:m:s Z').toDate().getTime()).to.be.within(now-timeout, now); - if (endpoint.startsWith('ws://locahost')) expect(callInfo.info.event.apiGatewayUrl).to.equal(endpoint.replace('ws://', 'http://').replace('wss://', 'https://')); - - // disconnect - c.ws.close(); - const disconnect=JSON.parse(await ws.receive1()); - now=Date.now(); - expectedCallInfo={id:c.id, event:{headers:createExpectedDisconnectHeaders(disconnect.info.event.headers), multiValueHeaders:createExpectedDisconnectMultiValueHeaders(disconnect.info.event.headers), ...createExpectedEvent(c.id, '$disconnect', 'DISCONNECT', disconnect.info.event)}, context:createExpectedContext(disconnect.info.context)}; - expect(disconnect).to.deep.equal({action:'update', event:'disconnect', info:expectedCallInfo}); - }).timeout(timeout); - - it('should be able to parse query string', async ()=>{ - const now=''+Date.now(); - const ws=await createWebSocket(); - await ws.send(JSON.stringify({action:'registerListener'})); - await ws.receive1(); - - const c1=await createClient(); - const c2=await createClient(`now=${now}&before=123456789`); - expect(JSON.parse(await ws.receive1()).info.event.queryStringParameters).to.be.undefined; - expect(JSON.parse(await ws.receive1()).info.event.queryStringParameters).to.deep.equal({now, before:'123456789'}); - }).timeout(timeout); - - it('should be able to receive messages via REST API', async ()=>{ - const c1=await createClient(); - const c2=await createClient(); - const url=new URL(endpoint); - const signature = {service: 'execute-api', host:url.host, path:`${url.pathname}/@connections/${c2.id}`, method: 'POST', body:'Hello World!', headers:{'Content-Type':'text/plain'/*'application/text'*/}}; - aws4.sign(signature, {accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey}); - const res=await req.post(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers['Authorization']).set('Content-Type', signature.headers['Content-Type']).send('Hello World!'); - expect(res).to.have.status(200); - expect(await c2.ws.receive1()).to.equal('Hello World!'); - }).timeout(timeout); - - it('should receive error code when sending to non existing client via REST API', async ()=>{ - const c='aJz0Md6VoAMCIbQ='; - const url=new URL(endpoint); - const signature = {service: 'execute-api', host:url.host, path:`${url.pathname}/@connections/${c}`, method: 'POST', body:'Hello World!', headers:{'Content-Type':'text/plain'/*'application/text'*/}}; - aws4.sign(signature, {accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey}); - const res=await req.post(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers['Authorization']).set('Content-Type', signature.headers['Content-Type']).send('Hello World!'); - expect(res).to.have.status(410); - }).timeout(timeout); - - // UNABLE TO TEST HIS SCENARIO BECAUSE AWS DOESN'T RETURN ANYTHING - // it('should not receive anything when POSTing nothing', async ()=>{ - // const c1=await createClient(); - // const url=new URL(endpoint); - // const signature = {service: 'execute-api', host:url.host, path:`${url.pathname}/@connections/${c1.id}`, method: 'POST'/*, body:'Hello World!'*/, headers:{'Content-Type':'text/plain'/*'application/text'*/}}; - // aws4.sign(signature, {accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey}); - // const res=await req.post(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers['Authorization']).set('Content-Type', signature.headers['Content-Type']).send(/*'Hello World!'*/); - // expect(res).to.have.status(200); - // }).timeout(timeout); - - }); -}); \ No newline at end of file diff --git a/manual_test_websocket/manual_test_websocket_main/test/support/WebSocketTester.js b/manual_test_websocket/manual_test_websocket_main/test/support/WebSocketTester.js deleted file mode 100644 index ee5c8d6fb..000000000 --- a/manual_test_websocket/manual_test_websocket_main/test/support/WebSocketTester.js +++ /dev/null @@ -1,60 +0,0 @@ -const WebSocket = require('ws'); - -class WebSocketTester { - constructor() { - this.messages=[]; this.receivers=[]; - } - - open(url) { - if (null!=this.ws) return; - const ws=this.ws=new WebSocket(url); - ws.on('message', (message)=>{ - // console.log('Received: '+message); - if (0 { - ws.on('open', ()=>{ - resolve(true); - }); - }); - } - - send(data) { - this.ws.send(data); - } - - receive1() { - return new Promise((resolve/*, reject*/)=>{ - if (0{ - const messages=[]; - for (let i=0; i{ - messages[i]=message; - if (i===n-1) resolve(messages); - }); - } - }); - } - - skip() { - if (0{}); - } - - countUnrecived() { - return this.messages.length; - } - - close() { - if (null!=this.ws) this.ws.close(); - } -}; - -module.exports=WebSocketTester; \ No newline at end of file From c38a4bc168a0c72e2f0aedb946a0f18001dd73ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20H=C3=A9rault?= Date: Sat, 22 Jun 2019 19:10:21 +0200 Subject: [PATCH 52/71] Display correct protocol on listen --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 95f70a111..53dcd352c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "serverless-offline", - "version": "5.3.3", + "version": "5.4.3", "lockfileVersion": 1, "requires": true, "dependencies": { From 9247070ca41a5046f1f904f69a0143c959d0622a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20H=C3=A9rault?= Date: Sat, 22 Jun 2019 19:12:36 +0200 Subject: [PATCH 53/71] Add replay last request feature --- src/ApiGateway.js | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/ApiGateway.js b/src/ApiGateway.js index adee630cb..a614e6d47 100644 --- a/src/ApiGateway.js +++ b/src/ApiGateway.js @@ -28,6 +28,7 @@ module.exports = class ApiGateway { this.exitCode = 0; this.requests = {}; + this.lastRequestOptions = null; this.velocityContextOptions = velocityContextOptions; } @@ -177,8 +178,20 @@ module.exports = class ApiGateway { process.exit(1); } + const protocol = `http${this.options.httpsProtocol ? 's' : ''}`; + this.printBlankLine(); - this.serverlessLog(`Offline [http] listening on http${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port}`); + this.serverlessLog(`Offline [${protocol}] listening on ${protocol}://${this.options.host}:${this.options.port}`); + this.serverlessLog('Enter "rp" to replay the last request'); + + process.openStdin().addListener('data', data => { + // note: data is an object, and when converted to a string it will + // end with a linefeed. so we (rather crudely) account for that + // with toString() and then trim() + if (data.toString().trim() === 'rp') { + this._injectLastRequest(); + } + }); } _createRoutes(event, funOptions, protectedRoutes, funName, servicePath, serviceRuntime, defaultContentType, key, fun) { @@ -260,6 +273,18 @@ module.exports = class ApiGateway { method: routeMethod, path: fullPath, handler: (request, h) => { // Here we go + // Store current request as the last one + this.lastRequestOptions = { + method: request.method, + url: request.url.href, + headers: request.headers, + payload: request.payload, + }; + + if (request.auth.credentials && request.auth.strategy) { + this.lastRequestOptions.auth = request.auth; + } + // Payload processing const encoding = detectEncoding(request); @@ -921,4 +946,14 @@ module.exports = class ApiGateway { return splittedStack.slice(0, splittedStack.findIndex(item => item.match(/server.route.handler.createLambdaContext/))).map(line => line.trim()); } + + _injectLastRequest() { + if (this.lastRequestOptions) { + this.serverlessLog('Replaying HTTP last request'); + this.server.inject(this.lastRequestOptions); + } + else { + this.serverlessLog('No last HTTP request to replay!'); + } + } }; From 719a14ee6600d21373c758dbd7b334a1019df226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20H=C3=A9rault?= Date: Sat, 22 Jun 2019 19:17:37 +0200 Subject: [PATCH 54/71] Fix http/https --> HTTP --- src/ApiGateway.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ApiGateway.js b/src/ApiGateway.js index a614e6d47..e6c74201e 100644 --- a/src/ApiGateway.js +++ b/src/ApiGateway.js @@ -178,10 +178,8 @@ module.exports = class ApiGateway { process.exit(1); } - const protocol = `http${this.options.httpsProtocol ? 's' : ''}`; - this.printBlankLine(); - this.serverlessLog(`Offline [${protocol}] listening on ${protocol}://${this.options.host}:${this.options.port}`); + this.serverlessLog(`Offline [HTTP] listening on http${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port}`); this.serverlessLog('Enter "rp" to replay the last request'); process.openStdin().addListener('data', data => { From 209a95e80824e510161034a0574d29cd123d8999 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20H=C3=A9rault?= Date: Sat, 22 Jun 2019 19:44:53 +0200 Subject: [PATCH 55/71] Use option to enable websocket feature Fix manual_test_websocket --- README.md | 3 ++- manual_test_websocket/.gitignore | 1 + .../RouteSelection/package.json | 2 +- .../scripts/deploy_to_offline.sh | 4 +-- .../{serverless.yml => serverless.yml.info} | 0 manual_test_websocket/main/package.json | 2 +- .../main/scripts/deploy_to_offline.sh | 4 +-- .../{serverless.yml => serverless.yml.info} | 0 manual_test_websocket/main/test/e2e/ws.e2e.js | 12 +++++++-- src/ApiGatewayWebSocket.js | 6 ++--- src/index.js | 27 +++++++++++++------ 11 files changed, 41 insertions(+), 20 deletions(-) create mode 100644 manual_test_websocket/.gitignore rename manual_test_websocket/RouteSelection/{serverless.yml => serverless.yml.info} (100%) rename manual_test_websocket/main/{serverless.yml => serverless.yml.info} (100%) diff --git a/README.md b/README.md index 9d35e6c89..72c0524e7 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,6 @@ All CLI options are optional: --location -l The root location of the handlers' files. Defaults to the current directory --host -o Host name to listen on. Default: localhost --port -P Port to listen on. Default: 3000 ---wsPort WebSocket port to listen on. Default: 3001 --stage -s The stage used to populate your templates. Default: the first stage found in your project. --region -r The region used to populate your templates. Default: the first region for the first stage found. --noTimeout -t Disables the timeout feature. @@ -101,6 +100,8 @@ All CLI options are optional: --providedRuntime Sets the runtime for "provided" lambda runtimes --disableModelValidation Disables the model validation --showDuration Show the execution time duration of the lambda function. +--useWebsocket Enable websocket routes. Default: false +--websocketPort WebSocket port to listen on. Default: the HTTP port + 1 ``` Any of the CLI options can be added to your `serverless.yml`. For example: diff --git a/manual_test_websocket/.gitignore b/manual_test_websocket/.gitignore new file mode 100644 index 000000000..fd983bc6b --- /dev/null +++ b/manual_test_websocket/.gitignore @@ -0,0 +1 @@ +/**/serverless.yml diff --git a/manual_test_websocket/RouteSelection/package.json b/manual_test_websocket/RouteSelection/package.json index e2e5c1f63..321caf40b 100644 --- a/manual_test_websocket/RouteSelection/package.json +++ b/manual_test_websocket/RouteSelection/package.json @@ -6,7 +6,7 @@ "scripts": { "test": "mocha ./test/*", "test-only": "mocha ", - "start": "sls offline", + "start": "sls offline --useWebsocket", "deploy-aws": "./scripts/deploy_to_aws.sh", "deploy-offline": "./scripts/deploy_to_offline.sh" }, diff --git a/manual_test_websocket/RouteSelection/scripts/deploy_to_offline.sh b/manual_test_websocket/RouteSelection/scripts/deploy_to_offline.sh index adbb643a3..31028b500 100755 --- a/manual_test_websocket/RouteSelection/scripts/deploy_to_offline.sh +++ b/manual_test_websocket/RouteSelection/scripts/deploy_to_offline.sh @@ -4,11 +4,11 @@ echo "Deploying to Offline ..." echo "Removing node modules ..." rm -fr ./node_modules echo "Instaing node modules ..." -npm i +npm i echo "Linking serverless-offline ..." npm link serverless-offline echo "Copying serverless.yml ..." cp ./scripts/serverless..yml ./serverless.yml cat ./scripts/serverless.offline.yml >> ./serverless.yml echo "Deploying to Offline ..." -sls offline +npm start diff --git a/manual_test_websocket/RouteSelection/serverless.yml b/manual_test_websocket/RouteSelection/serverless.yml.info similarity index 100% rename from manual_test_websocket/RouteSelection/serverless.yml rename to manual_test_websocket/RouteSelection/serverless.yml.info diff --git a/manual_test_websocket/main/package.json b/manual_test_websocket/main/package.json index e2e5c1f63..321caf40b 100644 --- a/manual_test_websocket/main/package.json +++ b/manual_test_websocket/main/package.json @@ -6,7 +6,7 @@ "scripts": { "test": "mocha ./test/*", "test-only": "mocha ", - "start": "sls offline", + "start": "sls offline --useWebsocket", "deploy-aws": "./scripts/deploy_to_aws.sh", "deploy-offline": "./scripts/deploy_to_offline.sh" }, diff --git a/manual_test_websocket/main/scripts/deploy_to_offline.sh b/manual_test_websocket/main/scripts/deploy_to_offline.sh index adbb643a3..31028b500 100755 --- a/manual_test_websocket/main/scripts/deploy_to_offline.sh +++ b/manual_test_websocket/main/scripts/deploy_to_offline.sh @@ -4,11 +4,11 @@ echo "Deploying to Offline ..." echo "Removing node modules ..." rm -fr ./node_modules echo "Instaing node modules ..." -npm i +npm i echo "Linking serverless-offline ..." npm link serverless-offline echo "Copying serverless.yml ..." cp ./scripts/serverless..yml ./serverless.yml cat ./scripts/serverless.offline.yml >> ./serverless.yml echo "Deploying to Offline ..." -sls offline +npm start diff --git a/manual_test_websocket/main/serverless.yml b/manual_test_websocket/main/serverless.yml.info similarity index 100% rename from manual_test_websocket/main/serverless.yml rename to manual_test_websocket/main/serverless.yml.info diff --git a/manual_test_websocket/main/test/e2e/ws.e2e.js b/manual_test_websocket/main/test/e2e/ws.e2e.js index 60f8a3706..d7c96b937 100644 --- a/manual_test_websocket/main/test/e2e/ws.e2e.js +++ b/manual_test_websocket/main/test/e2e/ws.e2e.js @@ -19,23 +19,31 @@ describe('serverless', () => { const createWebSocket = async qs => { const ws = new WebSocketTester(); let url = endpoint; + if (qs) url = `${endpoint}?${qs}`; + await ws.open(url); + clients.push(ws); return ws; }; const createClient = async qs => { const ws = await createWebSocket(qs); + ws.send(JSON.stringify({ action:'getClientInfo' })); + const json = await ws.receive1(); const id = JSON.parse(json).info.id; return { ws, id }; }; before(async () => { - req = chai.request(`${endpoint.replace('ws://', 'http://').replace('wss://', 'https://')}`).keepOpen(); - // req=chai.request('http://localhost:3001/dev').keepOpen(); + req = chai + .request(`${endpoint.replace('ws://', 'http://') + .replace('wss://', 'https://')}`) + .keepOpen(); + cred = await new Promise((resolve, reject) => { awscred.loadCredentials((err, data) => { if (err) reject(err); else resolve(data); diff --git a/src/ApiGatewayWebSocket.js b/src/ApiGatewayWebSocket.js index 58c64fefd..fb2cf1ff3 100644 --- a/src/ApiGatewayWebSocket.js +++ b/src/ApiGatewayWebSocket.js @@ -38,7 +38,7 @@ module.exports = class ApiGatewayWebSocket { // start COPY PASTE FROM HTTP SERVER CODE const serverOptions = { host: this.options.host, - port: this.options.wsPort, + port: this.options.websocketPort, router: { stripTrailingSlash: !this.options.preserveTrailingSlash, // removes trailing slashes on incoming paths. }, @@ -344,11 +344,11 @@ module.exports = class ApiGatewayWebSocket { await this.wsServer.start(); } catch (e) { - console.error(`Unexpected error while starting serverless-offline websocket server on port ${this.options.wsPort}:`, e); + console.error(`Unexpected error while starting serverless-offline websocket server on port ${this.options.websocketPort}:`, e); process.exit(1); } this.printBlankLine(); - this.serverlessLog(`Offline [websocket] listening on ws${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.wsPort}`); + this.serverlessLog(`Offline [websocket] listening on ws${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.websocketPort}`); } }; diff --git a/src/index.js b/src/index.js index 11d185bf0..ff78321d1 100755 --- a/src/index.js +++ b/src/index.js @@ -121,8 +121,11 @@ module.exports = class Offline { useSeparateProcesses: { usage: 'Uses separate node processes for handlers', }, - wsPort: { - usage: 'Websocket port to listen on. Default: 3001', + useWebsocket: { + usage: 'Enable websocket route. Default: false', + }, + websocketPort: { + usage: 'Websocket port to listen on. Default: the HTTP port + 1', }, }, }, @@ -153,7 +156,7 @@ module.exports = class Offline { return Promise.resolve(this._buildServer()) .then(() => this.apiGateway._listen()) - .then(() => this.apiGatewayWebSocket._listen()) + .then(() => this.options.useWebsocket && this.apiGatewayWebSocket._listen()) .then(() => this.options.exec ? this._executeShellScript() : this._listenForTermination()); } @@ -221,10 +224,13 @@ module.exports = class Offline { this._storeOriginalEnvironment(); // stores the original process.env for assigning upon invoking the handlers this.apiGateway = new ApiGateway(this.serverless, this.options, this.velocityContextOptions); - this.apiGatewayWebSocket = new ApiGatewayWebSocket(this.serverless, this.options); const server = this.apiGateway._createServer(); - this.apiGatewayWebSocket._createWebSocket(); + + if (this.options.useWebsocket) { + this.apiGatewayWebSocket = new ApiGatewayWebSocket(this.serverless, this.options); + this.apiGatewayWebSocket._createWebSocket(); + } this._setupEvents(); this.apiGateway._createResourceRoutes(); // HTTP Proxy defined in Resource @@ -268,7 +274,7 @@ module.exports = class Offline { resourceRoutes: false, skipCacheInvalidation: false, useSeparateProcesses: false, - wsPort: 3001, + useWebsocket: false, }; // In the constructor, stage and regions are set to undefined @@ -276,8 +282,13 @@ module.exports = class Offline { if (this.options.region === undefined) delete this.options.region; const yamlOptions = (this.service.custom || {})['serverless-offline']; + const { websocketPort } = this.options; this.options = Object.assign({}, defaultOptions, yamlOptions, this.options); + if (!websocketPort) { + this.options.websocketPort = this.options.port + 1; + } + // Prefix must start and end with '/' if (!this.options.prefix.startsWith('/')) this.options.prefix = `/${this.options.prefix}`; if (!this.options.prefix.endsWith('/')) this.options.prefix += '/'; @@ -394,7 +405,7 @@ module.exports = class Offline { // Adds a route for each http endpoint fun.events.forEach(event => { - if (event.websocket) { + if (this.options.useWebsocket && event.websocket) { experimentalWebSocketSupportWarning(); this.apiGatewayWebSocket._createWsAction(fun, funName, servicePath, funOptions, event); @@ -418,7 +429,7 @@ function experimentalWebSocketSupportWarning() { return; } - console.warn('WebSocket support in "serverless-offline" is experimental.\nFor any bugs, missing features or other feedback file an issue at https://github.com/dherault/serverless-offline/issues'); + console.warn('WebSocket support in serverless-offline is experimental.\nFor any bugs, missing features or other feedback file an issue at https://github.com/dherault/serverless-offline/issues'); experimentalWarningNotified = true; } From 69591080d6057f482d17415195ef04dfd2e626a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20H=C3=A9rault?= Date: Sat, 22 Jun 2019 19:53:00 +0200 Subject: [PATCH 56/71] Edit documentation --- README.md | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 72c0524e7..ef4f5068d 100644 --- a/README.md +++ b/README.md @@ -362,26 +362,32 @@ resources: To disable the model validation you can use `--disableModelValidation`. ## WebSocket -`This has experimental functionality. Please report any bugs or missing features.` -serverless-offline suports running a WebSocket local endpoint. +*This is an experimental functionality. Please report any bugs or missing features.* + +serverless-offline suports running a WebSocket local endpoint. To enable the feature pass the `--useWebsocket` option to the CLI. Usage in order to send messages back to clients: -`POST http://localhost:{port+1}/@connections/{connectionId}` +`POST http://localhost:{websocketPort}/@connections/{connectionId}` Or, +```js +const apiGatewayManagementApi = new AWS.ApiGatewayManagementApi({ + apiVersion: '2018-11-29', + endpoint: event.apiGatewayUrl || `${event.requestContext.domainName}/${event.requestContext.stage}`, +}); + +apiGatewayManagementApi.postToConnection({ + ConnectionId: ..., + Data: ..., +}); ``` -let endpoint=event.apiGatewayUrl; -if (!endpoint) endpoint = event.requestContext.domainName+'/'+event.requestContext.stage; -const apiVersion='2018-11-29'; -const apiGM=new API.ApiGatewayManagementApi({ apiVersion, endpoint }); -apiGM.postToConnection({ConnectionId, Data}); -``` + Where the `event` is received in the lambda handler function. -There's support for `websocketsApiRouteSelectionExpression` in it's basic form: `$request.body.x.y.z`, where the default value is `$request.body.action`. +There's support for [websocketsApiRouteSelectionExpression](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-selection-expressions.html) in it's basic form: `$request.body.x.y.z`, where the default value is `$request.body.action`. Authorizers and wss:// are currectly not supported in serverless-offline. From e73983c2171e9b1fcf09d0370fc0667021d8a503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20H=C3=A9rault?= Date: Sat, 22 Jun 2019 20:05:12 +0200 Subject: [PATCH 57/71] Fix travis build lint error --- manual_test_websocket/RouteSelection/test/e2e/ws.e2e.js | 1 - manual_test_websocket/main/test/e2e/ws.e2e.js | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/manual_test_websocket/RouteSelection/test/e2e/ws.e2e.js b/manual_test_websocket/RouteSelection/test/e2e/ws.e2e.js index fcd11c0be..ef07652ff 100644 --- a/manual_test_websocket/RouteSelection/test/e2e/ws.e2e.js +++ b/manual_test_websocket/RouteSelection/test/e2e/ws.e2e.js @@ -1,5 +1,4 @@ /* eslint-disable import/no-extraneous-dependencies */ - const chai = require('chai'); const WebSocketTester = require('../support/WebSocketTester'); diff --git a/manual_test_websocket/main/test/e2e/ws.e2e.js b/manual_test_websocket/main/test/e2e/ws.e2e.js index d7c96b937..5215a4485 100644 --- a/manual_test_websocket/main/test/e2e/ws.e2e.js +++ b/manual_test_websocket/main/test/e2e/ws.e2e.js @@ -1,4 +1,5 @@ /* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable import/no-unresolved */ /* eslint-disable no-unused-expressions */ const chai = require('chai'); const chaiHttp = require('chai-http'); @@ -6,6 +7,7 @@ const chaiHttp = require('chai-http'); chai.use(chaiHttp); const expect = chai.expect; const aws4 = require('aws4'); + const awscred = require('awscred'); const moment = require('moment'); From d6eae118ef8e0861960d1d8e318322a2fba1fae5 Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Tue, 25 Jun 2019 23:07:15 +0300 Subject: [PATCH 58/71] Fix a test that didn't run correctly on AWS Test: 'should receive error code when sending to non existing client via REST API' --- manual_test_websocket/main/test/e2e/ws.e2e.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/manual_test_websocket/main/test/e2e/ws.e2e.js b/manual_test_websocket/main/test/e2e/ws.e2e.js index 5215a4485..e2e503cbc 100644 --- a/manual_test_websocket/main/test/e2e/ws.e2e.js +++ b/manual_test_websocket/main/test/e2e/ws.e2e.js @@ -356,10 +356,12 @@ describe('serverless', () => { expect(await c2.ws.receive1()).to.equal('Hello World!'); }).timeout(timeout); - it('should receive error code when sending to non existing client via REST API', async () => { - const c = 'aJz0Md6VoAMCIbQ='; + it('should receive error code when sending to a recently closed client via REST API', async () => { + const c = await createClient(); + const cId = c.id; + c.ws.close(); const url = new URL(endpoint); - const signature = { service: 'execute-api', host:url.host, path:`${url.pathname}/@connections/${c}`, method: 'POST', body:'Hello World!', headers:{ 'Content-Type':'text/plain'/* 'application/text' */ } }; + const signature = { service: 'execute-api', host:url.host, path:`${url.pathname}/@connections/${cId}`, method: 'POST', body:'Hello World!', headers:{ 'Content-Type':'text/plain'/* 'application/text' */ } }; aws4.sign(signature, { accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey }); const res = await req.post(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers.Authorization).set('Content-Type', signature.headers['Content-Type']) .send('Hello World!'); From 5e19fa66f2627cf00056c469cbdd4b3becd51e03 Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Tue, 25 Jun 2019 23:34:06 +0300 Subject: [PATCH 59/71] Fixed test failing when running offline Test: 'should receive correct call info' --- manual_test_websocket/main/test/e2e/ws.e2e.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/manual_test_websocket/main/test/e2e/ws.e2e.js b/manual_test_websocket/main/test/e2e/ws.e2e.js index e2e503cbc..764ef16a0 100644 --- a/manual_test_websocket/main/test/e2e/ws.e2e.js +++ b/manual_test_websocket/main/test/e2e/ws.e2e.js @@ -286,7 +286,7 @@ describe('serverless', () => { return expected; }; - it('should receive correct call info', async () => { + it('should receive correct call info (event only)', async () => { const ws = await createWebSocket(); await ws.send(JSON.stringify({ action:'registerListener' })); await ws.receive1(); @@ -296,6 +296,7 @@ describe('serverless', () => { const connect = JSON.parse(await ws.receive1()); let now = Date.now(); let expectedCallInfo = { id:c.id, event:{ headers:createExpectedConnectHeaders(connect.info.event.headers), multiValueHeaders:createExpectedConnectMultiValueHeaders(connect.info.event.headers), ...createExpectedEvent(c.id, '$connect', 'CONNECT', connect.info.event) }, context:createExpectedContext(connect.info.context) }; + delete connect.info.context; delete expectedCallInfo.context; // Not checking context. Relying on it to be correct because serverless-offline uses general lambda context method expect(connect).to.deep.equal({ action:'update', event:'connect', info:expectedCallInfo }); expect(connect.info.event.requestContext.requestTimeEpoch).to.be.within(connect.info.event.requestContext.connectedAt - 10, connect.info.event.requestContext.requestTimeEpoch + 10); @@ -313,6 +314,7 @@ describe('serverless', () => { const callInfo = JSON.parse(await c.ws.receive1()); now = Date.now(); expectedCallInfo = { event:{ body: '{"action":"getCallInfo"}', ...createExpectedEvent(c.id, 'getCallInfo', 'MESSAGE', callInfo.info.event) }, context:createExpectedContext(callInfo.info.context) }; + delete callInfo.info.context; delete expectedCallInfo.context; // Not checking context. Relying on it to be correct because serverless-offline uses general lambda context method expect(callInfo).to.deep.equal({ action:'update', event:'call-info', info:expectedCallInfo }); expect(callInfo.info.event.requestContext.connectedAt).to.be.lt(callInfo.info.event.requestContext.requestTimeEpoch); @@ -326,7 +328,7 @@ describe('serverless', () => { const disconnect = JSON.parse(await ws.receive1()); now = Date.now(); expectedCallInfo = { id:c.id, event:{ headers:createExpectedDisconnectHeaders(disconnect.info.event.headers), multiValueHeaders:createExpectedDisconnectMultiValueHeaders(disconnect.info.event.headers), ...createExpectedEvent(c.id, '$disconnect', 'DISCONNECT', disconnect.info.event) }, context:createExpectedContext(disconnect.info.context) }; - + delete disconnect.info.context; delete expectedCallInfo.context; // Not checking context. Relying on it to be correct because serverless-offline uses general lambda context method expect(disconnect).to.deep.equal({ action:'update', event:'disconnect', info:expectedCallInfo }); }).timeout(timeout); From 0f742559ca0c7f142a041e976f002aa6b1c0c29b Mon Sep 17 00:00:00 2001 From: "@computerpunc" Date: Wed, 26 Jun 2019 10:56:30 +0300 Subject: [PATCH 60/71] Move to nodejs10.x runtime In order to remove the need to use an updated AWS sdk and so only to upload a few KB instead of ~10MB. --- manual_test_websocket/RouteSelection/package-lock.json | 6 +++--- manual_test_websocket/RouteSelection/package.json | 2 +- .../RouteSelection/scripts/deploy_to_aws.sh | 6 ++---- .../RouteSelection/scripts/deploy_to_offline.sh | 3 +-- .../RouteSelection/scripts/serverless..yml | 2 +- .../RouteSelection/scripts/serverless.aws.yml | 1 - manual_test_websocket/main/package-lock.json | 6 +++--- manual_test_websocket/main/package.json | 2 +- manual_test_websocket/main/scripts/deploy_to_aws.sh | 6 ++---- manual_test_websocket/main/scripts/deploy_to_offline.sh | 2 -- manual_test_websocket/main/scripts/serverless..yml | 2 +- manual_test_websocket/main/scripts/serverless.aws.yml | 1 - 12 files changed, 15 insertions(+), 24 deletions(-) diff --git a/manual_test_websocket/RouteSelection/package-lock.json b/manual_test_websocket/RouteSelection/package-lock.json index ba31f9664..00ea0b7b6 100644 --- a/manual_test_websocket/RouteSelection/package-lock.json +++ b/manual_test_websocket/RouteSelection/package-lock.json @@ -81,9 +81,9 @@ "dev": true }, "aws-sdk": { - "version": "2.473.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.473.0.tgz", - "integrity": "sha512-1Qr16lOcz4ANzl/oPQRR+fxchfvUx4PVQhUNnDU3FH9OBfU3Xj+Vh6bGYFbreFQgqIqXUTEuJR5pC44uK70YfA==", + "version": "2.482.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.482.0.tgz", + "integrity": "sha512-4MYfQZ+SETyjOUFZLEWVEhBqmxFi6MeI0X8FfAFhczb680+8PCKx/pWZHKLAR41k8+Lg5egM+fId0xtCaCSaeQ==", "requires": { "buffer": "4.9.1", "events": "1.1.1", diff --git a/manual_test_websocket/RouteSelection/package.json b/manual_test_websocket/RouteSelection/package.json index 321caf40b..105e99ffe 100644 --- a/manual_test_websocket/RouteSelection/package.json +++ b/manual_test_websocket/RouteSelection/package.json @@ -13,7 +13,7 @@ "author": "", "license": "MIT", "dependencies": { - "aws-sdk": "^2.473.0" + "aws-sdk": "^2.482.0" }, "devDependencies": { "aws4": "^1.8.0", diff --git a/manual_test_websocket/RouteSelection/scripts/deploy_to_aws.sh b/manual_test_websocket/RouteSelection/scripts/deploy_to_aws.sh index 590bef430..fa4733de7 100755 --- a/manual_test_websocket/RouteSelection/scripts/deploy_to_aws.sh +++ b/manual_test_websocket/RouteSelection/scripts/deploy_to_aws.sh @@ -1,10 +1,8 @@ #!/bin/bash echo "Deploying to AWS ..." -echo "Removing node modules ..." -rm -fr ./node_modules -echo "Instaing aws-sdk ..." -npm i aws-sdk +echo "Instaing node modules ..." +npm i echo "Copying serverless.yml ..." cp ./scripts/serverless..yml ./serverless.yml cat ./scripts/serverless.aws.yml >> ./serverless.yml diff --git a/manual_test_websocket/RouteSelection/scripts/deploy_to_offline.sh b/manual_test_websocket/RouteSelection/scripts/deploy_to_offline.sh index 31028b500..eb59d44b6 100755 --- a/manual_test_websocket/RouteSelection/scripts/deploy_to_offline.sh +++ b/manual_test_websocket/RouteSelection/scripts/deploy_to_offline.sh @@ -1,8 +1,6 @@ #!/bin/bash echo "Deploying to Offline ..." -echo "Removing node modules ..." -rm -fr ./node_modules echo "Instaing node modules ..." npm i echo "Linking serverless-offline ..." @@ -12,3 +10,4 @@ cp ./scripts/serverless..yml ./serverless.yml cat ./scripts/serverless.offline.yml >> ./serverless.yml echo "Deploying to Offline ..." npm start + diff --git a/manual_test_websocket/RouteSelection/scripts/serverless..yml b/manual_test_websocket/RouteSelection/scripts/serverless..yml index 0de4ad4e9..dee745070 100644 --- a/manual_test_websocket/RouteSelection/scripts/serverless..yml +++ b/manual_test_websocket/RouteSelection/scripts/serverless..yml @@ -15,7 +15,7 @@ service: manual-test-websocket-RouteSelection provider: name: aws - runtime: nodejs8.10 + runtime: nodejs10.x websocketsApiRouteSelectionExpression: $request.body.service.do diff --git a/manual_test_websocket/RouteSelection/scripts/serverless.aws.yml b/manual_test_websocket/RouteSelection/scripts/serverless.aws.yml index 64289757a..1844d0126 100644 --- a/manual_test_websocket/RouteSelection/scripts/serverless.aws.yml +++ b/manual_test_websocket/RouteSelection/scripts/serverless.aws.yml @@ -6,4 +6,3 @@ package: - ./** include: - handler.js - - node_modules/** diff --git a/manual_test_websocket/main/package-lock.json b/manual_test_websocket/main/package-lock.json index ba31f9664..85c7bd691 100644 --- a/manual_test_websocket/main/package-lock.json +++ b/manual_test_websocket/main/package-lock.json @@ -81,9 +81,9 @@ "dev": true }, "aws-sdk": { - "version": "2.473.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.473.0.tgz", - "integrity": "sha512-1Qr16lOcz4ANzl/oPQRR+fxchfvUx4PVQhUNnDU3FH9OBfU3Xj+Vh6bGYFbreFQgqIqXUTEuJR5pC44uK70YfA==", + "version": "2.481.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.481.0.tgz", + "integrity": "sha512-uwFGzwb2bKkh2KdX0nsebGqQNItZZ6j8+oL03jqSxCouO4FvFZpo8jd0ZnmkEeL6mWvv52WqV8HHhQNEyWkfNQ==", "requires": { "buffer": "4.9.1", "events": "1.1.1", diff --git a/manual_test_websocket/main/package.json b/manual_test_websocket/main/package.json index 321caf40b..4f0267d98 100644 --- a/manual_test_websocket/main/package.json +++ b/manual_test_websocket/main/package.json @@ -13,7 +13,7 @@ "author": "", "license": "MIT", "dependencies": { - "aws-sdk": "^2.473.0" + "aws-sdk": "^2.481.0" }, "devDependencies": { "aws4": "^1.8.0", diff --git a/manual_test_websocket/main/scripts/deploy_to_aws.sh b/manual_test_websocket/main/scripts/deploy_to_aws.sh index 590bef430..fa4733de7 100755 --- a/manual_test_websocket/main/scripts/deploy_to_aws.sh +++ b/manual_test_websocket/main/scripts/deploy_to_aws.sh @@ -1,10 +1,8 @@ #!/bin/bash echo "Deploying to AWS ..." -echo "Removing node modules ..." -rm -fr ./node_modules -echo "Instaing aws-sdk ..." -npm i aws-sdk +echo "Instaing node modules ..." +npm i echo "Copying serverless.yml ..." cp ./scripts/serverless..yml ./serverless.yml cat ./scripts/serverless.aws.yml >> ./serverless.yml diff --git a/manual_test_websocket/main/scripts/deploy_to_offline.sh b/manual_test_websocket/main/scripts/deploy_to_offline.sh index 31028b500..d5f8b4000 100755 --- a/manual_test_websocket/main/scripts/deploy_to_offline.sh +++ b/manual_test_websocket/main/scripts/deploy_to_offline.sh @@ -1,8 +1,6 @@ #!/bin/bash echo "Deploying to Offline ..." -echo "Removing node modules ..." -rm -fr ./node_modules echo "Instaing node modules ..." npm i echo "Linking serverless-offline ..." diff --git a/manual_test_websocket/main/scripts/serverless..yml b/manual_test_websocket/main/scripts/serverless..yml index 729d3ad14..d04a1792f 100644 --- a/manual_test_websocket/main/scripts/serverless..yml +++ b/manual_test_websocket/main/scripts/serverless..yml @@ -15,7 +15,7 @@ service: manual-test-websocket-main provider: name: aws - runtime: nodejs8.10 + runtime: nodejs10.x iamRoleStatements: - Effect: Allow diff --git a/manual_test_websocket/main/scripts/serverless.aws.yml b/manual_test_websocket/main/scripts/serverless.aws.yml index 64289757a..1844d0126 100644 --- a/manual_test_websocket/main/scripts/serverless.aws.yml +++ b/manual_test_websocket/main/scripts/serverless.aws.yml @@ -6,4 +6,3 @@ package: - ./** include: - handler.js - - node_modules/** From 7f56a18f265ada6afc88939110e96e18a5aa716e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20H=C3=A9rault?= Date: Sun, 30 Jun 2019 13:51:38 +0200 Subject: [PATCH 61/71] Remove websocket feature toggling Edit websocket default port (port + 1 --> 3001) Launch websocket server when needed --- README.md | 6 +----- src/index.js | 23 ++++++++--------------- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 9b5eec137..b06750ffa 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ All CLI options are optional: --location -l The root location of the handlers' files. Defaults to the current directory --host -o Host name to listen on. Default: localhost --port -P Port to listen on. Default: 3000 +--websocketPort WebSocket port to listen on. Default: 3001 --stage -s The stage used to populate your templates. Default: the first stage found in your project. --region -r The region used to populate your templates. Default: the first region for the first stage found. --noTimeout -t Disables the timeout feature. @@ -100,12 +101,7 @@ All CLI options are optional: --providedRuntime Sets the runtime for "provided" lambda runtimes --disableModelValidation Disables the model validation --showDuration Show the execution time duration of the lambda function. -<<<<<<< HEAD ---useWebsocket Enable websocket routes. Default: false ---websocketPort WebSocket port to listen on. Default: the HTTP port + 1 -======= --hideStackTraces Hide the stack trace on lambda failure. Default: false ->>>>>>> master ``` Any of the CLI options can be added to your `serverless.yml`. For example: diff --git a/src/index.js b/src/index.js index 31fa9a086..74aab29a4 100755 --- a/src/index.js +++ b/src/index.js @@ -124,9 +124,6 @@ module.exports = class Offline { useSeparateProcesses: { usage: 'Uses separate node processes for handlers', }, - useWebsocket: { - usage: 'Enable websocket route. Default: false', - }, websocketPort: { usage: 'Websocket port to listen on. Default: the HTTP port + 1', }, @@ -159,7 +156,7 @@ module.exports = class Offline { return Promise.resolve(this._buildServer()) .then(() => this.apiGateway._listen()) - .then(() => this.options.useWebsocket && this.apiGatewayWebSocket._listen()) + .then(() => this.hasWebsocketRoutes && this.apiGatewayWebSocket._listen()) .then(() => this.options.exec ? this._executeShellScript() : this._listenForTermination()); } @@ -230,10 +227,9 @@ module.exports = class Offline { const server = this.apiGateway._createServer(); - if (this.options.useWebsocket) { - this.apiGatewayWebSocket = new ApiGatewayWebSocket(this.serverless, this.options); - this.apiGatewayWebSocket._createWebSocket(); - } + this.hasWebsocketRoutes = false; + this.apiGatewayWebSocket = new ApiGatewayWebSocket(this.serverless, this.options); + this.apiGatewayWebSocket._createWebSocket(); this._setupEvents(); this.apiGateway._createResourceRoutes(); // HTTP Proxy defined in Resource @@ -267,6 +263,7 @@ module.exports = class Offline { noEnvironment: false, noTimeout: false, port: 3000, + websocketPort: 3001, prefix: '/', preserveTrailingSlash: false, printOutput: false, @@ -277,7 +274,6 @@ module.exports = class Offline { resourceRoutes: false, skipCacheInvalidation: false, useSeparateProcesses: false, - useWebsocket: false, hideStackTraces: false, }; @@ -286,13 +282,8 @@ module.exports = class Offline { if (this.options.region === undefined) delete this.options.region; const yamlOptions = (this.service.custom || {})['serverless-offline']; - const { websocketPort } = this.options; this.options = Object.assign({}, defaultOptions, yamlOptions, this.options); - if (!websocketPort) { - this.options.websocketPort = this.options.port + 1; - } - // Prefix must start and end with '/' if (!this.options.prefix.startsWith('/')) this.options.prefix = `/${this.options.prefix}`; if (!this.options.prefix.endsWith('/')) this.options.prefix += '/'; @@ -409,7 +400,9 @@ module.exports = class Offline { // Adds a route for each http endpoint fun.events.forEach(event => { - if (this.options.useWebsocket && event.websocket) { + if (event.websocket) { + this.hasWebsocketRoutes = true; + experimentalWebSocketSupportWarning(); this.apiGatewayWebSocket._createWsAction(fun, funName, servicePath, funOptions, event); From a77723e3aec4948b9dab39fc08365049d6e035e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20H=C3=A9rault?= Date: Sun, 30 Jun 2019 15:41:42 +0200 Subject: [PATCH 62/71] remove --useWebsocket options --- manual_test_websocket/RouteSelection/package.json | 2 +- manual_test_websocket/main/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/manual_test_websocket/RouteSelection/package.json b/manual_test_websocket/RouteSelection/package.json index 105e99ffe..25cf8b82d 100644 --- a/manual_test_websocket/RouteSelection/package.json +++ b/manual_test_websocket/RouteSelection/package.json @@ -6,7 +6,7 @@ "scripts": { "test": "mocha ./test/*", "test-only": "mocha ", - "start": "sls offline --useWebsocket", + "start": "sls offline", "deploy-aws": "./scripts/deploy_to_aws.sh", "deploy-offline": "./scripts/deploy_to_offline.sh" }, diff --git a/manual_test_websocket/main/package.json b/manual_test_websocket/main/package.json index 4f0267d98..86d946b13 100644 --- a/manual_test_websocket/main/package.json +++ b/manual_test_websocket/main/package.json @@ -6,7 +6,7 @@ "scripts": { "test": "mocha ./test/*", "test-only": "mocha ", - "start": "sls offline --useWebsocket", + "start": "sls offline", "deploy-aws": "./scripts/deploy_to_aws.sh", "deploy-offline": "./scripts/deploy_to_offline.sh" }, From 9d84707e4fb7d7f2234eb1d8b39bf5ae973c2ffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20H=C3=A9rault?= Date: Sun, 30 Jun 2019 17:24:13 +0200 Subject: [PATCH 63/71] lint --- .eslintrc.js | 1 + src/ApiGatewayWebSocket.js | 171 ++++++++++++++++--------------------- 2 files changed, 74 insertions(+), 98 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index cf14cb592..5dac96b4b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -8,6 +8,7 @@ const rules = { 'key-spacing': 'off', 'no-restricted-syntax': 'off', 'prefer-destructuring': 'off', + 'one-var-declaration-per-line': ['error', 'always'], semi: ['error', 'always'], strict: 'off', }; diff --git a/src/ApiGatewayWebSocket.js b/src/ApiGatewayWebSocket.js index fb2cf1ff3..276cb8e2a 100644 --- a/src/ApiGatewayWebSocket.js +++ b/src/ApiGatewayWebSocket.js @@ -7,7 +7,6 @@ const h2o2 = require('@hapi/h2o2'); const hapiPluginWebsocket = require('hapi-plugin-websocket'); const debugLog = require('./debugLog'); const createLambdaContext = require('./createLambdaContext'); -const createAuthScheme = require('./createAuthScheme'); const functionHelper = require('./functionHelper'); const { getUniqueId } = require('./utils'); const authFunctionNameExtractor = require('./authFunctionNameExtractor'); @@ -21,19 +20,10 @@ module.exports = class ApiGatewayWebSocket { this.options = options; this.exitCode = 0; this.clients = new Map(); - this.wsActions = {}; + this.actions = {}; this.websocketsApiRouteSelectionExpression = serverless.service.provider.websocketsApiRouteSelectionExpression || '$request.body.action'; } - printBlankLine() { - console.log(); - } - - logPluginIssue() { - this.serverlessLog('If you think this is an issue with the plugin please submit it, thanks!'); - this.serverlessLog('https://github.com/dherault/serverless-offline/issues'); - } - _createWebSocket() { // start COPY PASTE FROM HTTP SERVER CODE const serverOptions = { @@ -98,13 +88,18 @@ module.exports = class ApiGatewayWebSocket { this.wsServer.register(hapiPluginWebsocket).catch(err => err && this.serverlessLog(err)); - const doAction = (ws, connectionId, name, event, doDefaultAction/* , onError */) => { + const doAction = (ws, connectionId, name, event, doDefaultAction) => { const sendError = err => { - if (ws.readyState === /* OPEN */1) ws.send(JSON.stringify({ message:'Internal server error', connectionId, requestId:'1234567890' })); + if (ws.readyState === /* OPEN */1) { + ws.send(JSON.stringify({ message:'Internal server error', connectionId, requestId:'1234567890' })); + } + debugLog(`Error in handler of action ${action}`, err); }; - let action = this.wsActions[name]; - if (!action && doDefaultAction) action = this.wsActions.$default; + + let action = this.actions[name]; + + if (!action && doDefaultAction) action = this.actions.$default; if (!action) return; function cb(err) { @@ -120,6 +115,7 @@ module.exports = class ApiGatewayWebSocket { const context = createLambdaContext(func, this.service.provider, cb); let p = null; + try { p = action.handler(event, context, cb); } @@ -138,38 +134,38 @@ module.exports = class ApiGatewayWebSocket { method: 'POST', path: '/', config: { - payload: { output: 'data', parse: true, allow: 'application/json' }, + payload: { + output: 'data', + parse: true, + allow: 'application/json', + }, plugins: { websocket: { only: true, initially: false, connect: ({ ws, req }) => { - const parseQuery = queryString => { - const query = {}; const parts = queryString.split('?'); - if (parts.length < 2) return {}; - const pairs = parts[1].split('&'); - pairs.forEach(pair => { - const kv = pair.split('='); - query[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1] || ''); - }); - - return query; - }; - const queryStringParameters = parseQuery(req.url); const connection = { connectionId:getUniqueId(), connectionTime:Date.now() }; + debugLog(`connect:${connection.connectionId}`); this.clients.set(ws, connection); + let event = wsHelpers.createConnectEvent('$connect', 'CONNECT', connection, this.options); - if (Object.keys(queryStringParameters).length > 0) event = { queryStringParameters, ...event }; + + if (Object.keys(queryStringParameters).length > 0) { + event = { queryStringParameters, ...event }; + } doAction(ws, connection.connectionId, '$connect', event, false); }, disconnect: ({ ws }) => { const connection = this.clients.get(ws); + debugLog(`disconnect:${connection.connectionId}`); + this.clients.delete(ws); + const event = wsHelpers.createDisconnectEvent('$disconnect', 'DISCONNECT', connection, this.options); doAction(ws, connection.connectionId, '$disconnect', event, false); @@ -177,24 +173,40 @@ module.exports = class ApiGatewayWebSocket { }, }, }, - handler: (request, h) => { const { initially, ws } = request.websocket(); - if (!request.payload || initially) return h.response().code(204); + + if (!request.payload || initially) { + return h.response().code(204); + } + const connection = this.clients.get(ws); let actionName = null; + if (this.websocketsApiRouteSelectionExpression.startsWith('$request.body.')) { actionName = request.payload; + if (typeof actionName === 'object') { - this.websocketsApiRouteSelectionExpression.replace('$request.body.', '').split('.').forEach(key => { - if (actionName) actionName = actionName[key]; + this.websocketsApiRouteSelectionExpression + .replace('$request.body.', '') + .split('.') + .forEach(key => { + if (actionName) { + actionName = actionName[key]; + } }); } else actionName = null; } - if (typeof actionName !== 'string') actionName = null; + + if (typeof actionName !== 'string') { + actionName = null; + } + const action = actionName || '$default'; + debugLog(`action:${action} on connection=${connection.connectionId}`); + const event = wsHelpers.createEvent(action, 'MESSAGE', connection, request.payload, this.options); doAction(ws, connection.connectionId, action, event, true); @@ -215,6 +227,7 @@ module.exports = class ApiGatewayWebSocket { config: { payload: { parse: false } }, handler: (request, h) => { debugLog(`got POST to ${request.url}`); + const getByConnectionId = (map, searchValue) => { for (const [key, connection] of map.entries()) { if (connection.connectionId === searchValue) return key; @@ -224,10 +237,12 @@ module.exports = class ApiGatewayWebSocket { }; const ws = getByConnectionId(this.clients, request.params.connectionId); + if (!ws) return h.response().code(410); if (!request.payload) return ''; + ws.send(request.payload.toString()); - // console.log(`sent "${request.payload.toString().substring}" to ${request.params.connectionId}`); + debugLog(`sent data to connection:${request.params.connectionId}`); return ''; @@ -245,6 +260,7 @@ module.exports = class ApiGatewayWebSocket { const baseEnvironment = { AWS_REGION: 'dev', }; + if (!process.env.AWS_PROFILE) { baseEnvironment.AWS_ACCESS_KEY_ID = 'dev'; baseEnvironment.AWS_SECRET_ACCESS_KEY = 'dev'; @@ -260,17 +276,18 @@ module.exports = class ApiGatewayWebSocket { this.service.functions[funName].environment ); } + process.env._HANDLER = fun.handler; handler = functionHelper.createHandler(funOptions, this.options); } - catch (err) { - return this.serverlessLog(`Error while loading ${funName}`, err); + catch (error) { + return this.serverlessLog(`Error while loading ${funName}`, error); } const actionName = event.websocket.route; const action = { funName, fun, funOptions, servicePath, handler }; - this.wsActions[actionName] = action; + this.actions[actionName] = action; this.serverlessLog(`Action '${event.websocket.route}'`); } @@ -280,71 +297,13 @@ module.exports = class ApiGatewayWebSocket { return result.unsupportedAuth ? null : result.authorizerName; } - _configureAuthorization(endpoint, funName, method, epath, servicePath, serviceRuntime) { - if (!endpoint.authorizer) { - return null; - } - - const authFunctionName = this._extractAuthFunctionName(endpoint); - - if (!authFunctionName) { - return null; - } - - this.serverlessLog(`Configuring Authorization: ${endpoint.path} ${authFunctionName}`); - - const authFunction = this.service.getFunction(authFunctionName); - - if (!authFunction) return this.serverlessLog(`WARNING: Authorization function ${authFunctionName} does not exist`); - - const authorizerOptions = { - resultTtlInSeconds: '300', - identitySource: 'method.request.header.Authorization', - identityValidationExpression: '(.*)', - }; - - if (typeof endpoint.authorizer === 'string') { - authorizerOptions.name = authFunctionName; - } - else { - Object.assign(authorizerOptions, endpoint.authorizer); - } - - // Create a unique scheme per endpoint - // This allows the methodArn on the event property to be set appropriately - const authKey = `${funName}-${authFunctionName}-${method}-${epath}`; - const authSchemeName = `scheme-${authKey}`; - const authStrategyName = `strategy-${authKey}`; // set strategy name for the route config - - debugLog(`Creating Authorization scheme for ${authKey}`); - - // Create the Auth Scheme for the endpoint - const scheme = createAuthScheme( - authFunction, - authorizerOptions, - authFunctionName, - epath, - this.options, - this.serverlessLog, - servicePath, - serviceRuntime, - this.serverless - ); - - // Set the auth scheme and strategy on the server - this.server.auth.scheme(authSchemeName, scheme); - this.server.auth.strategy(authStrategyName, authSchemeName); - - return authStrategyName; - } - // All done, we can listen to incomming requests async _listen() { try { await this.wsServer.start(); } - catch (e) { - console.error(`Unexpected error while starting serverless-offline websocket server on port ${this.options.websocketPort}:`, e); + catch (error) { + console.error(`Unexpected error while starting serverless-offline websocket server on port ${this.options.websocketPort}:`, error); process.exit(1); } @@ -352,3 +311,19 @@ module.exports = class ApiGatewayWebSocket { this.serverlessLog(`Offline [websocket] listening on ws${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.websocketPort}`); } }; + +function parseQuery(queryString) { + const query = {}; + const parts = queryString.split('?'); + + if (parts.length < 2) return {}; + + const pairs = parts[1].split('&'); + + pairs.forEach(pair => { + const kv = pair.split('='); + query[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1] || ''); + }); + + return query; +} From f48978133fc84c99899e448ad9110a8cf1091cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20H=C3=A9rault?= Date: Sun, 30 Jun 2019 17:27:14 +0200 Subject: [PATCH 64/71] Update docs --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b06750ffa..50f5f9694 100644 --- a/README.md +++ b/README.md @@ -363,9 +363,7 @@ To disable the model validation you can use `--disableModelValidation`. ## WebSocket -*This is an experimental functionality. Please report any bugs or missing features.* - -serverless-offline suports running a WebSocket local endpoint. To enable the feature pass the `--useWebsocket` option to the CLI. +:warning: *This is an experimental functionality. Please report any bugs or missing features. PRs are welcome.* Usage in order to send messages back to clients: @@ -389,7 +387,7 @@ Where the `event` is received in the lambda handler function. There's support for [websocketsApiRouteSelectionExpression](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-selection-expressions.html) in it's basic form: `$request.body.x.y.z`, where the default value is `$request.body.action`. -Authorizers and wss:// are currectly not supported in serverless-offline. +Authorizers and wss:// are currectly not supported in this feature. ## Usage with Webpack From 27639ae95ac132f0a3bededcdc18750b64621719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Falala-Sechet?= Date: Tue, 2 Jul 2019 13:10:20 +0200 Subject: [PATCH 65/71] feat: add @connections DELETE route to close websocket --- src/ApiGatewayWebSocket.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/ApiGatewayWebSocket.js b/src/ApiGatewayWebSocket.js index 276cb8e2a..ceba881e4 100644 --- a/src/ApiGatewayWebSocket.js +++ b/src/ApiGatewayWebSocket.js @@ -248,6 +248,33 @@ module.exports = class ApiGatewayWebSocket { return ''; }, }); + + this.wsServer.route({ + method: 'DELETE', + path: '/@connections/{connectionId}', + config: { payload: { parse: false } }, + handler: (request, h) => { + debugLog(`got DELETE to ${request.url}`); + + const getByConnectionId = (map, searchValue) => { + for (const [key, connection] of map.entries()) { + if (connection.connectionId === searchValue) return key; + } + + return undefined; + }; + + const ws = getByConnectionId(this.clients, request.params.connectionId); + + if (!ws) return h.response().code(410); + + ws.close(); + + debugLog(`closed connection:${request.params.connectionId}`); + + return ''; + }, + }); } _createWsAction(fun, funName, servicePath, funOptions, event) { From d5e14aab25f3eabcf598204626c7713cda657ae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Falala-Sechet?= Date: Tue, 2 Jul 2019 13:10:35 +0200 Subject: [PATCH 66/71] fix: add missing printBlankLine method --- src/ApiGatewayWebSocket.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ApiGatewayWebSocket.js b/src/ApiGatewayWebSocket.js index ceba881e4..efd67b896 100644 --- a/src/ApiGatewayWebSocket.js +++ b/src/ApiGatewayWebSocket.js @@ -24,6 +24,10 @@ module.exports = class ApiGatewayWebSocket { this.websocketsApiRouteSelectionExpression = serverless.service.provider.websocketsApiRouteSelectionExpression || '$request.body.action'; } + printBlankLine() { + console.log(); + } + _createWebSocket() { // start COPY PASTE FROM HTTP SERVER CODE const serverOptions = { From e70db4b338385c5d21bf316aebb8ee9e4bb2881c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Falala-Sechet?= Date: Tue, 2 Jul 2019 13:19:53 +0200 Subject: [PATCH 67/71] chore: add tests for connection close --- manual_test_websocket/main/test/e2e/ws.e2e.js | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/manual_test_websocket/main/test/e2e/ws.e2e.js b/manual_test_websocket/main/test/e2e/ws.e2e.js index 764ef16a0..246f42873 100644 --- a/manual_test_websocket/main/test/e2e/ws.e2e.js +++ b/manual_test_websocket/main/test/e2e/ws.e2e.js @@ -370,5 +370,29 @@ describe('serverless', () => { expect(res).to.have.status(410); }).timeout(timeout); + + it('should be able to close connections via REST API', async () => { + await createClient(); + const c2 = await createClient(); + const url = new URL(endpoint); + const signature = { service: 'execute-api', host: url.host, path: `${url.pathname}/@connections/${c2.id}`, method: 'DELETE', body: 'Hello World!', headers: { 'Content-Type': 'text/plain'/* 'application/text' */ } }; + aws4.sign(signature, { accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey }); + const res = await req.del(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers.Authorization).set('Content-Type', signature.headers['Content-Type']); + + expect(res).to.have.status(200); + }).timeout(timeout); + + it('should receive error code when deleting a previously closed client via REST API', async () => { + const c = await createClient(); + const cId = c.id; + c.ws.close(); + const url = new URL(endpoint); + const signature = { service: 'execute-api', host: url.host, path: `${url.pathname}/@connections/${cId}`, method: 'DELETE', body: 'Hello World!', headers: { 'Content-Type': 'text/plain'/* 'application/text' */ } }; + aws4.sign(signature, { accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey }); + const res = await req.del(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers.Authorization).set('Content-Type', signature.headers['Content-Type']); + + expect(res).to.have.status(410); + }).timeout(timeout); + }); }); From b83c123939e62a1fbdf41504078e40dec6cc6b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Falala-Sechet?= Date: Tue, 2 Jul 2019 16:55:54 +0200 Subject: [PATCH 68/71] chore: if an error occurs during the $connect action, close the connection --- src/ApiGatewayWebSocket.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ApiGatewayWebSocket.js b/src/ApiGatewayWebSocket.js index 276cb8e2a..d48ecd5ab 100644 --- a/src/ApiGatewayWebSocket.js +++ b/src/ApiGatewayWebSocket.js @@ -94,6 +94,11 @@ module.exports = class ApiGatewayWebSocket { ws.send(JSON.stringify({ message:'Internal server error', connectionId, requestId:'1234567890' })); } + // mimic AWS behaviour (close connection) when the $connect action handler throws + if (name === '$connect') { + ws.close(); + } + debugLog(`Error in handler of action ${action}`, err); }; From c3fcc4c2cd5a72879a2d4f4e0f04f999740a9a96 Mon Sep 17 00:00:00 2001 From: Daniel Nalborczyk Date: Tue, 2 Jul 2019 20:17:21 -0400 Subject: [PATCH 69/71] Rename getUniqueId to createUniqueId --- src/ApiGateway.js | 4 ++-- src/ApiGatewayWebSocket.js | 4 ++-- src/createLambdaContext.js | 4 ++-- src/createLambdaProxyContext.js | 4 ++-- src/createVelocityContext.js | 4 ++-- src/functionHelper.js | 4 ++-- src/utils.js | 2 +- src/websocketHelpers.js | 12 ++++++------ 8 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/ApiGateway.js b/src/ApiGateway.js index ccf418e18..767124122 100644 --- a/src/ApiGateway.js +++ b/src/ApiGateway.js @@ -15,7 +15,7 @@ const createAuthScheme = require('./createAuthScheme'); const functionHelper = require('./functionHelper'); const Endpoint = require('./Endpoint'); const parseResources = require('./parseResources'); -const { detectEncoding, getUniqueId } = require('./utils'); +const { detectEncoding, createUniqueId } = require('./utils'); const authFunctionNameExtractor = require('./authFunctionNameExtractor'); const requestBodyValidator = require('./requestBodyValidator'); @@ -345,7 +345,7 @@ module.exports = class ApiGateway { } } // Shared mutable state is the root of all evil they say - const requestId = getUniqueId(); + const requestId = createUniqueId(); this.requests[requestId] = { done: false }; this.currentRequestId = requestId; diff --git a/src/ApiGatewayWebSocket.js b/src/ApiGatewayWebSocket.js index 9a329dc21..22d8ccb94 100644 --- a/src/ApiGatewayWebSocket.js +++ b/src/ApiGatewayWebSocket.js @@ -8,7 +8,7 @@ const hapiPluginWebsocket = require('hapi-plugin-websocket'); const debugLog = require('./debugLog'); const createLambdaContext = require('./createLambdaContext'); const functionHelper = require('./functionHelper'); -const { getUniqueId } = require('./utils'); +const { createUniqueId } = require('./utils'); const authFunctionNameExtractor = require('./authFunctionNameExtractor'); const wsHelpers = require('./websocketHelpers'); @@ -154,7 +154,7 @@ module.exports = class ApiGatewayWebSocket { initially: false, connect: ({ ws, req }) => { const queryStringParameters = parseQuery(req.url); - const connection = { connectionId:getUniqueId(), connectionTime:Date.now() }; + const connection = { connectionId:createUniqueId(), connectionTime:Date.now() }; debugLog(`connect:${connection.connectionId}`); diff --git a/src/createLambdaContext.js b/src/createLambdaContext.js index eebd95616..7976da2ff 100644 --- a/src/createLambdaContext.js +++ b/src/createLambdaContext.js @@ -1,6 +1,6 @@ 'use strict'; -const { getUniqueId } = require('./utils'); +const { createUniqueId } = require('./utils'); // https://docs.aws.amazon.com/lambda/latest/dg/limits.html // default function timeout in seconds @@ -26,7 +26,7 @@ module.exports = function createLambdaContext(fun, provider, cb) { getRemainingTimeInMillis: () => endTime - new Date().getTime(), // properties - awsRequestId: `offline_awsRequestId_${getUniqueId()}`, + awsRequestId: `offline_awsRequestId_${createUniqueId()}`, clientContext: {}, functionName, functionVersion: `offline_functionVersion_for_${functionName}`, diff --git a/src/createLambdaProxyContext.js b/src/createLambdaProxyContext.js index 643785e25..ba0dbb5b6 100644 --- a/src/createLambdaProxyContext.js +++ b/src/createLambdaProxyContext.js @@ -5,7 +5,7 @@ const { normalizeMultiValueQuery, normalizeQuery, nullIfEmpty, - getUniqueId, + createUniqueId, } = require('./utils'); /* @@ -79,7 +79,7 @@ module.exports = function createLambdaProxyContext(request, options, stageVariab resourceId: 'offlineContext_resourceId', apiId: 'offlineContext_apiId', stage: options.stage, - requestId: `offlineContext_requestId_${getUniqueId()}`, + requestId: `offlineContext_requestId_${createUniqueId()}`, identity: { cognitoIdentityPoolId: process.env.SLS_COGNITO_IDENTITY_POOL_ID || 'offlineContext_cognitoIdentityPoolId', accountId: process.env.SLS_ACCOUNT_ID || 'offlineContext_accountId', diff --git a/src/createVelocityContext.js b/src/createVelocityContext.js index 013f2b511..7a747b7cd 100644 --- a/src/createVelocityContext.js +++ b/src/createVelocityContext.js @@ -2,7 +2,7 @@ const jsEscapeString = require('js-string-escape'); const { decode } = require('jsonwebtoken'); -const { isPlainObject, getUniqueId } = require('./utils'); +const { isPlainObject, createUniqueId } = require('./utils'); const jsonPath = require('./jsonPath'); function escapeJavaScript(x) { @@ -66,7 +66,7 @@ module.exports = function createVelocityContext(request, options, payload) { userAgent: request.headers['user-agent'] || '', userArn: 'offlineContext_userArn', }, - requestId: `offlineContext_requestId_${getUniqueId()}`, + requestId: `offlineContext_requestId_${createUniqueId()}`, resourceId: 'offlineContext_resourceId', resourcePath: request.route.path, stage: options.stage, diff --git a/src/functionHelper.js b/src/functionHelper.js index c42a368a0..9d7bcc6b8 100644 --- a/src/functionHelper.js +++ b/src/functionHelper.js @@ -4,7 +4,7 @@ const { fork, spawn } = require('child_process'); const path = require('path'); const trimNewlines = require('trim-newlines'); const debugLog = require('./debugLog'); -const { getUniqueId } = require('./utils'); +const { createUniqueId } = require('./utils'); const handlerCache = {}; const messageCallbacks = {}; @@ -150,7 +150,7 @@ module.exports = { } return (event, context, done) => { - const id = getUniqueId(); + const id = createUniqueId(); messageCallbacks[id] = done; handlerContext.inflight.add(id); handlerContext.process.send(Object.assign({}, funOptions, { id, event, context })); diff --git a/src/utils.js b/src/utils.js index af46d883c..9ba07d491 100644 --- a/src/utils.js +++ b/src/utils.js @@ -43,7 +43,7 @@ module.exports = { return createHash('md5').digest('hex'); }, - getUniqueId() { + createUniqueId() { return cuid(); }, }; diff --git a/src/websocketHelpers.js b/src/websocketHelpers.js index 36805636a..80113f828 100644 --- a/src/websocketHelpers.js +++ b/src/websocketHelpers.js @@ -1,5 +1,5 @@ const { DateTime } = require('luxon'); -const { getUniqueId } = require('./utils'); +const { createUniqueId } = require('./utils'); // TODO this should be probably moved to utils, and combined with other header // functions and utilities @@ -34,7 +34,7 @@ const createRequestContext = (action, eventType, connection) => { connectionId:connection.connectionId, domainName: 'localhost', eventType, - extendedRequestId: `${getUniqueId()}`, + extendedRequestId: `${createUniqueId()}`, identity: { accountId: null, accessKey: null, @@ -50,8 +50,8 @@ const createRequestContext = (action, eventType, connection) => { userArn: null, }, messageDirection: 'IN', - messageId: `${getUniqueId()}`, - requestId: `${getUniqueId()}`, + messageId: `${createUniqueId()}`, + requestId: `${createUniqueId()}`, requestTime: formatToClfTime(now), requestTimeEpoch: now.getTime(), routeKey: action, @@ -76,9 +76,9 @@ exports.createConnectEvent = (action, eventType, connection, options) => { const headers = { Host: 'localhost', 'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits', - 'Sec-WebSocket-Key': `${getUniqueId()}`, + 'Sec-WebSocket-Key': `${createUniqueId()}`, 'Sec-WebSocket-Version': '13', - 'X-Amzn-Trace-Id': `Root=${getUniqueId()}`, + 'X-Amzn-Trace-Id': `Root=${createUniqueId()}`, 'X-Forwarded-For': '127.0.0.1', 'X-Forwarded-Port': `${options.port + 1}`, 'X-Forwarded-Proto': `http${options.httpsProtocol ? 's' : ''}`, From c0bea4c5b63665a069510d0e59d96450207fe079 Mon Sep 17 00:00:00 2001 From: Daniel Nalborczyk Date: Wed, 3 Jul 2019 08:09:40 -0400 Subject: [PATCH 70/71] Remove apiGatewayUrl from websocket event --- src/ApiGatewayWebSocket.js | 4 ++-- src/websocketHelpers.js | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/ApiGatewayWebSocket.js b/src/ApiGatewayWebSocket.js index 22d8ccb94..ad5b18f37 100644 --- a/src/ApiGatewayWebSocket.js +++ b/src/ApiGatewayWebSocket.js @@ -175,7 +175,7 @@ module.exports = class ApiGatewayWebSocket { this.clients.delete(ws); - const event = wsHelpers.createDisconnectEvent('$disconnect', 'DISCONNECT', connection, this.options); + const event = wsHelpers.createDisconnectEvent('$disconnect', 'DISCONNECT', connection); doAction(ws, connection.connectionId, '$disconnect', event, false); }, @@ -216,7 +216,7 @@ module.exports = class ApiGatewayWebSocket { debugLog(`action:${action} on connection=${connection.connectionId}`); - const event = wsHelpers.createEvent(action, 'MESSAGE', connection, request.payload, this.options); + const event = wsHelpers.createEvent(action, 'MESSAGE', connection, request.payload); doAction(ws, connection.connectionId, action, event, true); diff --git a/src/websocketHelpers.js b/src/websocketHelpers.js index 80113f828..531f310c9 100644 --- a/src/websocketHelpers.js +++ b/src/websocketHelpers.js @@ -61,9 +61,8 @@ const createRequestContext = (action, eventType, connection) => { return requestContext; }; -exports.createEvent = (action, eventType, connection, payload, options) => { +exports.createEvent = (action, eventType, connection, payload) => { const event = { - apiGatewayUrl: `http${options.httpsProtocol ? 's' : ''}://${options.host}:${options.port + 1}`, body: JSON.stringify(payload), isBase64Encoded: false, requestContext: createRequestContext(action, eventType, connection), @@ -85,7 +84,6 @@ exports.createConnectEvent = (action, eventType, connection, options) => { }; const multiValueHeaders = createMultiValueHeaders(headers); const event = { - apiGatewayUrl: `http${options.httpsProtocol ? 's' : ''}://${options.host}:${options.port + 1}`, headers, isBase64Encoded: false, multiValueHeaders, @@ -95,7 +93,7 @@ exports.createConnectEvent = (action, eventType, connection, options) => { return event; }; -exports.createDisconnectEvent = (action, eventType, connection, options) => { +exports.createDisconnectEvent = (action, eventType, connection) => { const headers = { Host: 'localhost', 'x-api-key': '', @@ -103,7 +101,6 @@ exports.createDisconnectEvent = (action, eventType, connection, options) => { }; const multiValueHeaders = createMultiValueHeaders(headers); const event = { - apiGatewayUrl: `http${options.httpsProtocol ? 's' : ''}://${options.host}:${options.port + 1}`, headers, isBase64Encoded: false, multiValueHeaders, From f901507ed70fe8da1fea0544a392c759f2b23be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Falala-Sechet?= Date: Wed, 3 Jul 2019 22:31:16 +0200 Subject: [PATCH 71/71] fix: don't add body and content-type headers to sigv4 for delete call in tests --- manual_test_websocket/main/test/e2e/ws.e2e.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/manual_test_websocket/main/test/e2e/ws.e2e.js b/manual_test_websocket/main/test/e2e/ws.e2e.js index 246f42873..22bca2728 100644 --- a/manual_test_websocket/main/test/e2e/ws.e2e.js +++ b/manual_test_websocket/main/test/e2e/ws.e2e.js @@ -375,9 +375,9 @@ describe('serverless', () => { await createClient(); const c2 = await createClient(); const url = new URL(endpoint); - const signature = { service: 'execute-api', host: url.host, path: `${url.pathname}/@connections/${c2.id}`, method: 'DELETE', body: 'Hello World!', headers: { 'Content-Type': 'text/plain'/* 'application/text' */ } }; + const signature = { service: 'execute-api', host: url.host, path: `${url.pathname}/@connections/${c2.id}`, method: 'DELETE' }; aws4.sign(signature, { accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey }); - const res = await req.del(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers.Authorization).set('Content-Type', signature.headers['Content-Type']); + const res = await req.del(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers.Authorization); expect(res).to.have.status(200); }).timeout(timeout); @@ -387,9 +387,9 @@ describe('serverless', () => { const cId = c.id; c.ws.close(); const url = new URL(endpoint); - const signature = { service: 'execute-api', host: url.host, path: `${url.pathname}/@connections/${cId}`, method: 'DELETE', body: 'Hello World!', headers: { 'Content-Type': 'text/plain'/* 'application/text' */ } }; + const signature = { service: 'execute-api', host: url.host, path: `${url.pathname}/@connections/${cId}`, method: 'DELETE' }; aws4.sign(signature, { accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey }); - const res = await req.del(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers.Authorization).set('Content-Type', signature.headers['Content-Type']); + const res = await req.del(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers.Authorization); expect(res).to.have.status(410); }).timeout(timeout);