diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e0a631..a9f6218 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ### 2.3.2 (Next) +* [#32](https://github.com/alexa-js/alexa-app-server/pull/32): Bug fixes and more testing/coverage - [@tejashah88](https://github.com/tejashah88) + * added testing for request verification, HTTPS support, and POST-based routes + * fixed potential memory leaks from not closing the HTTPS server instance and not removing the hotswap listeners + * now using alexa-verifier-middleware for request verification + * changed loading location of contents of 'sslcert' folder (should be part of 'examples' folder) + * fixed documentation for generating the SSL certificate * [#28](https://github.com/alexa-js/alexa-app-server/pull/28): Moved to the [alexa-js organization](https://github.com/alexa-js) - [@dblock](https://github.com/dblock). * [#23](https://github.com/alexa-js/alexa-app-server/pull/23): Added `server.stop()` - [@dblock](https://github.com/dblock). * [#23](https://github.com/alexa-js/alexa-app-server/pull/23): Added LICENSE - [@dblock](https://github.com/dblock). @@ -29,5 +35,4 @@ ### 2.2.2 (Aug 18, 2015) -* Changed `preRequest()` and `postRequest()` to allow them to return a `Promise` if they perform async operations - [@matt-kruse](https://github.com/matt-kruse). - +* Changed `preRequest()` and `postRequest()` to allow them to return a `Promise` if they perform async operations - [@matt-kruse](https://github.com/matt-kruse). \ No newline at end of file diff --git a/README.md b/README.md index 13e7ee9..92ab483 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ require('alexa-app-server').start({ httpsPort: 443, // privateKey filename. This file must reside in the sslcert folder under the root of the project. Must be set if httpsEnable = true - privateKey: 'private-key.key', + privateKey: 'private-key.pem', // certificate filename. This file must reside in the sslcert folder under the root of the project. Must be set if httpsEnable = true certificate: 'cert.cer' @@ -161,7 +161,7 @@ Generate a x509 SSL Certificate using the following: ``` openssl genrsa -out private-key.pem 1024 -openssl req -new -x509 -key private-key.pem -out cert.cer -days 365 --generates the certificate +openssl req -new -x509 -key private-key.pem -out cert.cer -days 365 ``` Then add the following properties the to config (currently in server.js) that creates the server. Place the two generated files in the sslcert directory. diff --git a/examples/sslcert/cert.cer b/examples/sslcert/cert.cer new file mode 100644 index 0000000..203618f --- /dev/null +++ b/examples/sslcert/cert.cer @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICsDCCAhmgAwIBAgIJAK0FG9fi8e69MA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTcwMTEzMjM0MzE0WhcNMTgwMTEzMjM0MzE0WjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB +gQDVIqwTKz6mm/i588dB/fz11IAgsgi/Cu9RvgE8H6ps7Le2kf9RAA857sPJtoCJ +GeZwNqlV1EYUH8/RGHAuWALoe6RDBW7kFdW9J1b3IbrjGqEr3hdPNa6MgoukH/Mz +QAbBCE4F9hoRRa6p4G2vnxi3nzUEA0ND4GHUscLvNWbWdwIDAQABo4GnMIGkMB0G +A1UdDgQWBBRfaLJKSAWD773NxNzTRWz/0jx0KTB1BgNVHSMEbjBsgBRfaLJKSAWD +773NxNzTRWz/0jx0KaFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUt +U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAK0FG9fi +8e69MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAuUqQCqkjZFykLx3J +u1hzmFP8qR49chLQgqArS2JwkTqqUfj5tPn0b6DWxedfAINa8sbc/+pebSTgLcHe ++7N61aZaQuAz0N48c+jaHu8AOMIbDxRkTtjz7KXQL5dHLzXV70EUcNtNtFBxM4L7 +Zp4V5VmyPwS+tli292hEOTDRQGE= +-----END CERTIFICATE----- diff --git a/examples/sslcert/private-key.pem b/examples/sslcert/private-key.pem new file mode 100644 index 0000000..3c7a507 --- /dev/null +++ b/examples/sslcert/private-key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQDVIqwTKz6mm/i588dB/fz11IAgsgi/Cu9RvgE8H6ps7Le2kf9R +AA857sPJtoCJGeZwNqlV1EYUH8/RGHAuWALoe6RDBW7kFdW9J1b3IbrjGqEr3hdP +Na6MgoukH/MzQAbBCE4F9hoRRa6p4G2vnxi3nzUEA0ND4GHUscLvNWbWdwIDAQAB +AoGAftLb26gu5osG7PePSMhuvoUNHOdzZuKF13kdWP5qtdgB1WR4rWVAqjNWU3AC +ehJsWbdc+dKPRKhNS9mj3x/F0iSGOUvVwBRU7SvfuJLSp+z0Pk4gh+aJz/6R3Rh+ +xiUyeqFJksS4k1qKCIXehXAGoTW/XLRDUVG+hATHbC73gMECQQD0FeHOBFW51dXr +eM0IY3VW9s1JdYMYt+jgb/Jg7DU/aJ48+LoVkk2/hieI3sQff3wBIf8hN7vgo0Ny +MPu23VAXAkEA34oJADoum4MdAy7i8Q9Ap6c5Kp1cBaJ75tcuhHBlWWZiQy6QmZqi +8yDQpTKWMb+Z+YP5Kul7vhK6KeC0/XdIoQJBAPLpjA2BlucY/ooXcMV2ZeKkP+1p +e4xwCtzBzE/VA7EVJtW7G0Y4khOXKWU3fatzLi/aa5PdaabIFGligj+cxQUCQQCG +i7i7MEnZRGN0BQaHfVy3DEm2Qpyer5vP53iSMmxuENfYA/D440BtAjVTGU2Zh++P +ZUXV9E6MqwzuI9gML33BAkAHWuxlPZmjmOj0DYV50yT0w8WZiaWeUpl6h/pEOtEq +M1F+ioph2YJrWeEY4GbOsXnbm+CEoN8mfiAhFZvTyykW +-----END RSA PRIVATE KEY----- diff --git a/index.js b/index.js index 50557b8..3a0323f 100644 --- a/index.js +++ b/index.js @@ -5,8 +5,8 @@ var http = require('http'); var https = require('https'); var express = require('express'); var alexa = require('alexa-app'); -var verifier = require('alexa-verifier'); var bodyParser = require('body-parser'); +var alexaVerifierMiddleware = require('alexa-verifier-middleware'); var Promise = require('bluebird'); var appServer = function(config) { @@ -23,12 +23,16 @@ var appServer = function(config) { self.error = function(msg) { console.log(msg); }; // Configure hotswap to watch for changes and swap out module code - hotswap.on('swap', function(filename) { + var hotswapCallback = function(filename) { self.log("hotswap reloaded " + filename); - }); - hotswap.on('error', function(e) { + }; + + var errorCallback = function(e) { self.log("-----\nhotswap error: " + e + "\n-----\n"); - }); + }; + + hotswap.on('swap', hotswapCallback); + hotswap.on('error', errorCallback); // Load application modules self.load_apps = function(app_dir, root) { @@ -71,29 +75,9 @@ var appServer = function(config) { // so bootstrap manually to express var endpoint = (root || '/') + (app.endpoint || app.name); if (config.verify) { - self.express.use(endpoint, function(req, res, next) { - req.verified = false; - if (!req.headers.signaturecertchainurl) { - return next(); - } - - var cert_url = req.headers.signaturecertchainurl; - var signature = req.headers.signature; - var requestBody = req.rawBody; - verifier(cert_url, signature, requestBody, function(er) { - if (er) { - res.status(401).json({ status: 'failure', reason: er }); - } else { - req.verified = true; - next(); - } - }); - }); + self.express.use(endpoint, alexaVerifierMiddleware()); } self.express.post(endpoint, function(req, res) { - if (config.verify && !req.verified) { - res.status(401).json({ status: 'failure', reason: 'Unauthorized' }); - } var json = req.body, response_json; // preRequest may return altered request JSON, or undefined, or a Promise @@ -218,8 +202,8 @@ var appServer = function(config) { self.log("httpsEnabled is true. Reading HTTPS config"); if (config.privateKey != undefined && config.certificate != undefined && config.httpsPort != undefined) { //Ensure that all of the needed properties are set - var privateKeyFile = 'sslcert/' + config.privateKey; - var certificateFile = 'sslcert/' + config.certificate; + var privateKeyFile = server_root + '/sslcert/' + config.privateKey; + var certificateFile = server_root + '/sslcert/' + config.certificate; if (fs.existsSync(privateKeyFile) && fs.existsSync(certificateFile)) { //Make sure the key and cert exist. @@ -230,7 +214,7 @@ var appServer = function(config) { var credentials = { key: privateKey, cert: certificate }; try { //The line below can fail it the certs were generated incorrectly. But we can continue startup without HTTPS - https.createServer(credentials, self.express).listen(config.httpsPort); //create the HTTPS server + self.httpsInstance = https.createServer(credentials, self.express).listen(config.httpsPort); //create the HTTPS server self.log("Listening on HTTPS port " + config.httpsPort); } catch (error) { self.log("Failed to listen via HTTPS Error: " + error); @@ -252,7 +236,7 @@ var appServer = function(config) { } // Start the server listening config.port = config.port || process.env.port || 80; - self.instance = self.express.listen(config.port); + self.httpInstance = self.express.listen(config.port); self.log("Listening on HTTP port " + config.port); // Run the post() method if defined @@ -264,7 +248,16 @@ var appServer = function(config) { }; self.stop = function() { - self.instance.close(); + // close all server instances + self.httpInstance.close(); + + if (typeof self.httpsInstance !== "undefined") { + self.httpsInstance.close(); + } + + // deactivate all hotswap listener + hotswap.removeListener('swap', hotswapCallback); + hotswap.removeListener('error', errorCallback); }; return self; @@ -277,4 +270,4 @@ appServer.start = function(config) { return appServerInstance; }; -module.exports = appServer; +module.exports = appServer; \ No newline at end of file diff --git a/package.json b/package.json index 3d46a0e..d7fd6d9 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "license": "MIT", "dependencies": { "alexa-app": "^2.4.0", - "alexa-verifier": "0.0.5", + "alexa-verifier-middleware": "^0.1.4", "bluebird": "^2.9.34", "body-parser": "~1.0.1", "ejs": "~0.7.1", diff --git a/test/test-examples-server-https-support.js b/test/test-examples-server-https-support.js new file mode 100644 index 0000000..dfe8b75 --- /dev/null +++ b/test/test-examples-server-https-support.js @@ -0,0 +1,52 @@ +/*jshint expr: true*/ +"use strict"; +var chai = require("chai"); +var expect = chai.expect; +chai.config.includeStack = true; +var request = require("supertest-as-promised"); + +describe("Alexa App Server with Examples & HTTPS support", function() { + var testServer; + + beforeEach(function() { + testServer = require("../index").start({ + port: 3000, + server_root: 'examples', + httpsEnabled: true, + httpsPort: 6000, + privateKey: 'private-key.pem', + certificate: 'cert.cer' + }); + }); + + afterEach(function() { + testServer.stop(); + }); + + it("starts an express instance", function() { + return request(testServer.express) + .get('/') + .expect(200).then(function(response) { + expect(response.text).to.contain("alexa-app-server is running"); + } + ); + }); + + it("mounts hello world app", function() { + return request(testServer.express) + .get('/alexa/helloworld') + .expect(200) + }); + + it("mounts number_guessing_game", function() { + return request(testServer.express) + .get('/alexa/guessinggame') + .expect(200) + }); + + it("404s on an invalid app", function() { + return request(testServer.express) + .get('/alexa/invalid') + .expect(404) + }); +}); \ No newline at end of file diff --git a/test/test-examples-server-verification.js b/test/test-examples-server-verification.js new file mode 100644 index 0000000..0ca56d9 --- /dev/null +++ b/test/test-examples-server-verification.js @@ -0,0 +1,49 @@ +/*jshint expr: true*/ +"use strict"; +var chai = require("chai"); +var expect = chai.expect; +chai.config.includeStack = true; +var request = require("supertest-as-promised"); + +describe("Alexa App Server with Examples & Verification", function() { + var testServer; + + beforeEach(function() { + testServer = require("../index").start({ + port: 3000, + server_root: 'examples', + verify: true + }); + }); + + afterEach(function() { + testServer.stop(); + }); + + it("starts an express instance", function() { + return request(testServer.express) + .get('/') + .expect(200).then(function(response) { + expect(response.text).to.contain("alexa-app-server is running"); + } + ); + }); + + it("mounts hello world app", function() { + return request(testServer.express) + .get('/alexa/helloworld') + .expect(200) + }); + + it("mounts number_guessing_game", function() { + return request(testServer.express) + .get('/alexa/guessinggame') + .expect(200) + }); + + it("404s on an invalid app", function() { + return request(testServer.express) + .get('/alexa/invalid') + .expect(404) + }); +}); \ No newline at end of file diff --git a/test/test-examples-server.js b/test/test-examples-server.js index 8574d9e..7f5b62d 100644 --- a/test/test-examples-server.js +++ b/test/test-examples-server.js @@ -8,6 +8,26 @@ var request = require("supertest-as-promised"); describe("Alexa App Server with Examples", function() { var testServer; + var sampleLaunchReq = { + "version": "1.0", + "session": { + "new": true, + "sessionId": "amzn1.echo-api.session.abeee1a7-aee0-41e6-8192-e6faaed9f5ef", + "application": { + "applicationId": "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe" + }, + "attributes": {}, + "user": { + "userId": "amzn1.account.AM3B227HF3FAM1B261HK7FFM3A2" + } + }, + "request": { + "type": "LaunchRequest", + "requestId": "amzn1.echo-api.request.9cdaa4db-f20e-4c58-8d01-c75322d6c423", + "timestamp": "2015-05-13T12:34:56Z" + } + }; + beforeEach(function() { testServer = require("../index").start({ port: 3000, @@ -28,21 +48,42 @@ describe("Alexa App Server with Examples", function() { ); }); - it("mounts hello world app", function() { + it("mounts hello world app (GET)", function() { return request(testServer.express) .get('/alexa/helloworld') .expect(200) }); - it("mounts number_guessing_game", function() { + it("mounts hello world app (POST)", function() { + return request(testServer.express) + .post('/alexa/helloworld') + .send(sampleLaunchReq) + .expect(200) + }); + + it("mounts number_guessing_game (GET)", function() { return request(testServer.express) .get('/alexa/guessinggame') .expect(200) }); - it("404s on an invalid app", function() { + it("mounts number_guessing_game (POST)", function() { + return request(testServer.express) + .post('/alexa/guessinggame') + .send(sampleLaunchReq) + .expect(200) + }); + + it("404s on an invalid app (GET)", function() { return request(testServer.express) .get('/alexa/invalid') .expect(404) }); + + it("404s on an invalid app (POST)", function() { + return request(testServer.express) + .post('/alexa/invalid') + .send(sampleLaunchReq) + .expect(404) + }); });