Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Chris Schneider authored and Chris Schneider committed Jun 29, 2017
0 parents commit 92b2249
Show file tree
Hide file tree
Showing 13 changed files with 628 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*.DS_Store
*.log
.nyc_output/
node_modules/
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
language: node_js
node_js:
- '6.0'
- '6.10'
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2017 DieProduktMacher GmbH

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
92 changes: 92 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
Serverless Local Dev Server Plugin (Beta)
=======

[![Build Status](https://travis-ci.org/DieProduktMacher/serverless-local-dev-server.svg?branch=develop)](https://travis-ci.org/DieProduktMacher/serverless-local-dev-server)

This plugin exposes your Alexa-Skill and HTTP functions as local HTTP endpoints, removing the need to deploy every change to AWS Lambda. You can connect these endpoints to Alexa or services like Messenger Bots via forwardhq, ngrok or any other forwarding tool.

Supported features:

* Expose `alexa-skill` and `http` events as HTTP endpoints
* Environment variables
* Very basic HTTP integration
* Auto reload via nodemon (see *How To*)

This package requires node >= 6.0


# How To

### 1. Install the plugin

```sh
npm install serverless-local-dev-server --save-dev
```

### 2. Add the plugin to your serverless configuration file

*serverless.yml* configuration example:

```yaml
provider:
name: aws
runtime: nodejs6.10

functions:
hello:
handler: handler.hello
events:
- alexaSkill
- http: GET /

# Add serverless-local-dev-server to your plugins:
plugins:
- serverless-local-dev-server
```
### 3. Start the server
```sh
serverless local-dev-server
```

On default the server listens on port 5005. You can specify another one with the *--port* argument:

```sh
serverless local-dev-server --port 5000
```

To automatically restart the server when files change, you may use nodemon:

```sh
nodemon --exec "serverless local-dev-server" -e "js yml json"
```

To see responses returned from Lambda and stack traces, prepend SLS_DEBUG=*

```sh
SLS_DEBUG=* serverless local-http-server
```

### 4. For Alexa Skills

#### 4.1 Share localhost with the internet

For example with forwardhq:

```sh
forward 5005
```

#### 4.2 Configure AWS to use your HTTPS endpoint

In the Configuration pane, select HTTPS as service endpoint type and specify the forwarded endpoint URL.

As method for SSL Certificate validation select *My development endpoint is a sub-domain of a domain that has a wildcard certificate from a certificate authority*.


# License & Credits

Licensed under the MIT license.

Created and maintained by [DieProduktMacher](http://www.dieproduktmacher.com).
44 changes: 44 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "serverless-local-http-server",
"version": "0.1.0",
"engines": {
"node": ">=6.0"
},
"description": "Develop Alexa-Skill and HTTP functions in Serverless without deploying to AWS",
"author": "DieProduktMacher <www.dieproduktmacher.com>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/DieProduktMacher/serverless-local-http-server"
},
"keywords": [
"serverless",
"serverless-plugin",
"alexa",
"alexa-skill",
"http",
"development",
"dev",
"local",
"aws-lambda"
],
"main": "src/index.js",
"scripts": {
"test": "nyc mocha",
"lint": "standard"
},
"dependencies": {
"body-parser": "^1.17.2",
"express": "^4.15.3"
},
"devDependencies": {
"chai": "^4.0.0",
"chai-as-promised": "^7.0.0",
"mocha": "^3.4.2",
"node-fetch": "^1.7.1",
"nyc": "^10.3.2",
"serverless": "^1.14.0",
"sinon": "^2.3.2",
"standard": "^10.0.2"
}
}
93 changes: 93 additions & 0 deletions src/Server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use strict'

const Express = require('express')
const BodyParser = require('body-parser')
const path = require('path')
const getEndpoints = require('./endpoints/get')

class Server {
constructor () {
this.functions = []
this.defaultEnvironment = Object.assign({}, process.env)
this.log = console.log
}
// Starts the server
start (port) {
if (this.functions.length === 0) {
this.log('No Lambdas with Alexa-Skill or HTTP events found')
return
}
this.app = Express()
this.app.use(BodyParser.json())
this.functions.forEach(func =>
func.endpoints.forEach(endpoint => this._attachEndpoint(func, endpoint))
)
this.app.listen(port, _ => {
this.log(`Listening on port ${port} for requests 🚀`)
this.log('----')
this.functions.forEach(func => {
this.log(`${func.name}:`)
func.endpoints.forEach(endpoint => {
this.log(` ${endpoint.method} http://localhost:${port}${endpoint.path}`)
})
})
this.log('----')
})
}
// Sets functions, including endpoints, using the serverless config and service path
setFunctions (serverlessConfig, servicePath) {
this.functions = Object.keys(serverlessConfig.functions).map(name => {
let functionConfig = serverlessConfig.functions[name]
let handlerParts = functionConfig.handler.split('.')
return {
name: name,
config: serverlessConfig.functions[name],
handlerModulePath: path.join(servicePath, handlerParts[0]),
handlerFunctionName: handlerParts[1],
environment: Object.assign({}, serverlessConfig.provider.environment, functionConfig.environment)
}
}).map(func =>
Object.assign({}, func, { endpoints: getEndpoints(func) })
).filter(func =>
func.endpoints.length > 0
)
}
// Attaches HTTP endpoint to Express
_attachEndpoint (func, endpoint) {
// Validate method and path
/* istanbul ignore next */
if (!endpoint.method || !endpoint.path) {
return this.log(`Endpoint ${endpoint.type} for function ${func.name} has no method or path`)
}
// Add HTTP endpoint to Express
this.app[endpoint.method.toLowerCase()](endpoint.path, (request, response) => {
this.log(`${endpoint}`)
// Execute Lambda with corresponding event, forward response to Express
let lambdaEvent = endpoint.getLambdaEvent(request)
this._executeLambdaHandler(func, lambdaEvent).then(result => {
this.log(' ➡ Success')
if (process.env.SLS_DEBUG) console.info(result)
endpoint.handleLambdaSuccess(response, result)
}).catch(error => {
this.log(` ➡ Failure: ${error.message}`)
if (process.env.SLS_DEBUG) console.error(error.stack)
endpoint.handleLambdaFailure(response, error)
})
})
}
// Loads and executes the Lambda handler
_executeLambdaHandler (func, event) {
return new Promise((resolve, reject) => {
// Load function and variables
let handle = require(func.handlerModulePath)[func.handlerFunctionName]
let context = { succeed: resolve, fail: reject }
let callback = (error, result) => (!error) ? resolve(result) : reject(error)
// Set new environment variables, execute handler function
process.env = Object.assign({ IS_OFFLINE: true }, func.environment, this.defaultEnvironment)
handle(event, context, callback)
process.env = Object.assign({}, this.defaultEnvironment)
})
}
}

module.exports = Server
24 changes: 24 additions & 0 deletions src/endpoints/AlexaSkillEndpoint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use strict'

const Endpoint = require('./Endpoint')

class AlexaSkillEndpoint extends Endpoint {
constructor (alexaSkillConfig, func) {
super(alexaSkillConfig, func)
this.name = func.name
this.method = 'POST'
this.path = `/alexa-skill/${this.name}`
}
getLambdaEvent (request) {
// Pass-through
return request.body
}
handleLambdaSuccess (response, result) {
response.send(result)
}
toString () {
return `Alexa-Skill: ${this.name}`
}
}

module.exports = AlexaSkillEndpoint
22 changes: 22 additions & 0 deletions src/endpoints/Endpoint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict'

class Endpoint {
constructor (eventConfig, func) {
this.method = 'GET'
this.path = ''
}
/* istanbul ignore next */
getLambdaEvent (request) {
return {}
}
/* istanbul ignore next */
handleLambdaSuccess (response, result) {
response.sendStatus(204)
}
/* istanbul ignore next */
handleLambdaFailure (response, error) {
response.sendStatus(500)
}
}

module.exports = Endpoint
36 changes: 36 additions & 0 deletions src/endpoints/HttpEndpoint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use strict'

const path = require('path')
const Endpoint = require('./Endpoint')

class HttpEndpoint extends Endpoint {
constructor (httpConfig, func) {
super(httpConfig, func)
if (typeof httpConfig === 'string') {
let s = httpConfig.split(' ')
httpConfig = { method: s[0], path: s[1] }
}
this.method = httpConfig.method
this.resourcePath = httpConfig.path.replace(/\{([a-zA-Z_]+)\}/g, ':$1')
this.path = path.join('/http', this.resourcePath)
}
getLambdaEvent (request) {
return {
httpMethod: request.method,
body: JSON.stringify(request.body, null, ' '),
queryStringParameters: request.query
}
}
handleLambdaSuccess (response, result) {
if (result.headers) {
response.set(result.headers)
}
response.status(result.statusCode)
response.send(result.body === 'object' ? JSON.stringify(result.body) : result.body)
}
toString () {
return `HTTP: ${this.method} ${this.resourcePath}`
}
}

module.exports = HttpEndpoint
27 changes: 27 additions & 0 deletions src/endpoints/get.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use strict'

const mappings = {
'alexaSkill': require('./AlexaSkillEndpoint'),
'http': require('./HttpEndpoint')
}

module.exports = (func) => {
return func.config.events.map(event => {
switch (typeof event) {
case 'string':
return { type: event, config: {} }
case 'object':
let type = Object.keys(event)[0]
return { type: type, config: event[type] }
/* istanbul ignore next */
default:
return null
}
}).filter(_ =>
!!mappings[_.type]
).map(_ => {
let endpoint = new mappings[_.type](_.config, func)
endpoint.type = _.type
return endpoint
})
}
Loading

0 comments on commit 92b2249

Please sign in to comment.