├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples ├── webhook-server-as-express-app.js └── webhook-server.js ├── index.js ├── lib └── server.js ├── package.json └── test └── server.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .idea 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Jurgen Van de Moere 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Contentful webhook server 2 | 3 | [![Build Status](https://travis-ci.org/jvandemo/contentful-webhook-server.svg?branch=master)](https://travis-ci.org/jvandemo/contentful-webhook-server) 4 | 5 | Webhooks in [Contentful](https://www.contentful.com) notify you when content in your space has changed. 6 | 7 | Contentful webhook server is a lightweight server to handle these notifications: 8 | 9 | - handles incoming [Contentful webhook HTTP requests](https://www.contentful.com/developers/documentation/content-delivery-api/#webhooks) 10 | - emits events for all Contentful webhook topics to allow easy webhook handling 11 | - supports username/password authentication 12 | - supports all default [node HTTP server](https://nodejs.org/api/http.html) options 13 | 14 | ## Installation 15 | 16 | ```bash 17 | $ npm install contentful-webhook-server 18 | ``` 19 | 20 | ## Quick example 21 | 22 | ```javascript 23 | // Create webhook server 24 | var server = require('contentful-webhook-server')({ 25 | path: '/', 26 | username: 'user', 27 | password: 'pass' 28 | }); 29 | 30 | // Attach handlers to Contentful webhooks 31 | server.on('ContentManagement.ContentType.publish', function(req){ 32 | console.log('A content type was published!'); 33 | }); 34 | 35 | // Start listening for requests on port 3000 36 | server.listen(3000, function(){ 37 | console.log('Contentful webhook server running on port ' + 3000) 38 | }); 39 | 40 | ``` 41 | 42 | ## Configuration 43 | 44 | You can pass a configuration object when instantiating the server: 45 | 46 | ```javascript 47 | // Create webhook server 48 | var server = require('contentful-webhook-server')({ 49 | path: '/', 50 | username: 'user', 51 | password: 'pass' 52 | }); 53 | ``` 54 | 55 | where: 56 | 57 | - **path**: the path you want the server to listen on, default: '/' 58 | - **username**: the username you expect the request to contain, default: '' 59 | - **password**: the password you expect the request to contain, default: '' 60 | 61 | So to start a server on `localhost:3000` without authentication, you can: 62 | 63 | ```javascript 64 | // Create server with default options 65 | var server = require('contentful-webhook-server')(); 66 | 67 | // Start listening for requests on port 3000 68 | server.listen(3000, function(){ 69 | console.log('Contentful webhook server running on port ' + 3000) 70 | }); 71 | ``` 72 | 73 | and to start a server on `localhost:3000/webhooks` with authentication, you can: 74 | 75 | ```javascript 76 | // Create server with default options 77 | var server = require('contentful-webhook-server')({ 78 | path: '/webhooks', 79 | username: 'user', 80 | password: 'pass' 81 | }); 82 | 83 | // Start listening for requests on port 3000 84 | server.listen(3000, function(){ 85 | console.log('Contentful webhook server running on port ' + 3000) 86 | }); 87 | ``` 88 | 89 | ## Handling incoming webhook requests 90 | 91 | The server emits incoming Contentful webhook topics as event, so you can: 92 | 93 | ```javascript 94 | server.on('ContentManagement.ContentType.publish', function(req){ 95 | console.log('A content type was published!'); 96 | }); 97 | 98 | server.on('ContentManagement.ContentType.unpublish', function(req){ 99 | console.log('A content type was unpublished!'); 100 | }); 101 | 102 | server.on('ContentManagement.Entry.publish', function(req){ 103 | console.log('An entry was published!'); 104 | }); 105 | 106 | server.on('ContentManagement.Entry.unpublish', function(req){ 107 | console.log('An entry was unpublished!'); 108 | }); 109 | 110 | server.on('ContentManagement.Asset.publish', function(req){ 111 | console.log('An asset was published!'); 112 | }); 113 | 114 | server.on('ContentManagement.Asset.unpublish', function(req){ 115 | console.log('An asset was unpublished!'); 116 | }); 117 | ``` 118 | 119 | > This module does not make any assumptions about your application and does **NOT** attempt to parse or extract the contents of the request. 120 | 121 | > Instead it passes the [request](https://nodejs.org/api/http.html#http_http_incomingmessage) to your handler(s) so you can process (or ignore) the contents of the [incoming message](https://nodejs.org/api/http.html#http_http_incomingmessage) from within your handler(s). 122 | 123 | 124 | ## Special wildcard event 125 | 126 | The server emits a special wildcard event too in case you want to listen to all events in one go: 127 | 128 | ```javascript 129 | 130 | // Handler for all successful requests 131 | // Is not emitted when an error occurs 132 | server.on('ContentManagement.*', function(topic, req){ 133 | 134 | // topic is available as string 135 | // => e.g. ContentManagement.Asset.unpublish 136 | console.log('Request came in for: ' + topic); 137 | }); 138 | ``` 139 | 140 | > This event is only emitted on successful requests, not on errors 141 | 142 | ## Handling errors and invalid requests 143 | 144 | When an invalid request comes in, a `ContentManagement.error` event is emitted: 145 | 146 | ```javascript 147 | // Handle errors 148 | server.on('ContentManagement.error', function(err, req){ 149 | console.log(err); 150 | }); 151 | ``` 152 | 153 | ## Simulating a request using curl 154 | 155 | If you want to try out your server during development, you can simulate a request without credentials using cUrl: 156 | 157 | ```bash 158 | $ curl -X POST --header "X-Contentful-Topic: ContentManagement.Entry.publish" localhost:3000 159 | ``` 160 | 161 | and simulate requests with authentication like this: 162 | 163 | ```bash 164 | $ curl -X POST -u user:pass --header "X-Contentful-Topic: ContentManagement.Entry.publish" localhost:3000 165 | ``` 166 | 167 | ## Enabling webhooks in Contentful 168 | 169 | To enable webhooks in your Contentful space, go to your space settings and fill in the options you specified in your server configuration: 170 | 171 | ![contentful-webhook](https://cloud.githubusercontent.com/assets/1859381/7337492/fc2b25e6-ec2b-11e4-99ef-ddaba53e77a6.png) 172 | 173 | As soon as you save the webhook in Contentful, your server will start receiving notifications. 174 | 175 | ## Example 176 | 177 | A working example is included [here](examples/webhook-server.js). 178 | 179 | ## License 180 | 181 | MIT 182 | 183 | ## Change log 184 | 185 | ### 1.2.0 186 | 187 | - Added ability to mount as middleware 188 | 189 | ### 1.1.0 190 | 191 | - Added working example 192 | - Updated documentation 193 | 194 | ### 1.0.0 195 | 196 | - Added authentication support 197 | - Updated documentation 198 | 199 | ### 0.2.0 200 | 201 | - Added unit tests 202 | - Updated documentation 203 | 204 | ### 0.1.0 205 | 206 | - Initial version 207 | -------------------------------------------------------------------------------- /examples/webhook-server-as-express-app.js: -------------------------------------------------------------------------------- 1 | // To test locally with curl, run: 2 | // $ curl --data "test=data" localhost:3000/webhook --header "x-contentful-topic: ContentManagement.ContentType.publish" 3 | 4 | var express = require('express'); 5 | var webhookServer = require('../lib/server')(); 6 | 7 | var port = process.env.PORT || 3000; 8 | 9 | var app = express(); 10 | 11 | app.get('/', function (req, res) { 12 | res.send('Express response'); 13 | }); 14 | 15 | app.use('/webhook', webhookServer.mountAsMiddleware); 16 | 17 | var server = app.listen(port, function () { 18 | console.log('Started on port ' + port); 19 | }); 20 | 21 | webhookServer.on('ContentManagement.ContentType.publish', function (req) { 22 | console.log('A content type was published!'); 23 | }); 24 | 25 | webhookServer.on('ContentManagement.ContentType.unpublish', function (req) { 26 | console.log('A content type was unpublished!'); 27 | }); 28 | 29 | webhookServer.on('ContentManagement.Entry.publish', function (req) { 30 | console.log('An entry was published!'); 31 | }); 32 | 33 | webhookServer.on('ContentManagement.Entry.unpublish', function (req) { 34 | console.log('An entry was unpublished!'); 35 | }); 36 | 37 | webhookServer.on('ContentManagement.Asset.publish', function (req) { 38 | console.log('An asset was published!'); 39 | }); 40 | 41 | webhookServer.on('ContentManagement.Asset.unpublish', function (req) { 42 | console.log('An asset was unpublished!'); 43 | }); 44 | -------------------------------------------------------------------------------- /examples/webhook-server.js: -------------------------------------------------------------------------------- 1 | // To test locally with curl, run: 2 | // $ curl --data "test=data" localhost:3000 --header "x-contentful-topic: ContentManagement.ContentType.publish" 3 | 4 | var server = require('../index.js')({ 5 | path: '/', 6 | username: 'user', 7 | password: 'pass' 8 | }); 9 | 10 | server.on('ContentManagement.error', function (err, req) { 11 | console.log(err); 12 | }); 13 | 14 | server.on('ContentManagement.ContentType.publish', function (req) { 15 | console.log('ContentManagement.ContentType.publish'); 16 | }); 17 | 18 | server.on('ContentManagement.*', function (topic, req) { 19 | console.log('*: ' + topic); 20 | }); 21 | 22 | server.listen(3000, function () { 23 | console.log('Contentful webhook server running on port ' + 3000) 24 | }); 25 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/server.js'); 2 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var auth = require('basic-auth'); 3 | 4 | module.exports = createServer; 5 | 6 | var httpServer; 7 | var options = { 8 | path: '/', 9 | username: '', 10 | password: '' 11 | }; 12 | 13 | var HEADERS = { 14 | TOPIC: 'x-contentful-topic' 15 | }; 16 | 17 | var EVENTS = { 18 | ERROR: 'ContentManagement.error', 19 | ALL_SUCCESSFUL_REQUESTS: 'ContentManagement.*' 20 | }; 21 | 22 | function createServer(opts) { 23 | if (opts && opts.path) { 24 | options.path = opts.path; 25 | } 26 | if (opts && opts.username) { 27 | options.username = opts.username; 28 | } 29 | if (opts && opts.password) { 30 | options.password = opts.password; 31 | } 32 | httpServer = http.createServer(handleRequest); 33 | httpServer.mountAsMiddleware = handleRequest; 34 | return httpServer 35 | } 36 | 37 | function handleRequest(req, res) { 38 | var path = req.url.split('?').shift(); 39 | var topic = req.headers[HEADERS.TOPIC]; 40 | var err; 41 | var incomingUsername = ''; 42 | var incomingPassword = ''; 43 | var incomingCredentials; 44 | 45 | // Handle non matching path 46 | if (path !== options.path) { 47 | err = new Error('The request does not match the path'); 48 | httpServer.emit(EVENTS.ERROR, err, req); 49 | return404(); 50 | return; 51 | } 52 | 53 | // Handle incorrect methods 54 | if (req.method !== 'POST') { 55 | err = new Error('The request did not use the POST method'); 56 | httpServer.emit(EVENTS.ERROR, err, req); 57 | return404(); 58 | return; 59 | } 60 | 61 | // Handle missing topic 62 | if (!topic) { 63 | return return400('X-Contentful-Topic not specified in request'); 64 | } 65 | 66 | // Verify credentials 67 | incomingCredentials = auth(req); 68 | if (incomingCredentials) { 69 | incomingUsername = incomingCredentials.name; 70 | incomingPassword = incomingCredentials.pass; 71 | } 72 | if ((incomingUsername !== options.username) || (incomingPassword !== options.password)) { 73 | err = new Error('Unauthorized: request contained invalid credentials') 74 | httpServer.emit(EVENTS.ERROR, err, req, incomingCredentials); 75 | return returnAccessDenied(); 76 | } 77 | 78 | // Emit topic and wildcard event 79 | httpServer.emit(topic, req); 80 | httpServer.emit(EVENTS.ALL_SUCCESSFUL_REQUESTS, topic, req); 81 | 82 | // Return 200 OK 83 | res.writeHead(200, {'content-type': 'application/json'}); 84 | res.end('{"ok":true}'); 85 | 86 | function return400(message) { 87 | res.writeHead(400, {'content-type': 'application/json'}); 88 | res.end(JSON.stringify({error: message})); 89 | } 90 | 91 | function returnAccessDenied() { 92 | res.writeHead(401, {'WWW-Authenticate': 'Basic realm="Contentful webhooks"'}); 93 | res.end(); 94 | } 95 | 96 | function return404() { 97 | res.statusCode = 404; 98 | res.end(); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contentful-webhook-server", 3 | "version": "1.2.0", 4 | "description": "Server to handle Contentful webhook HTTP requests", 5 | "keywords": [ 6 | "contentful", 7 | "webhook" 8 | ], 9 | "author": { 10 | "name": "Jurgen Van de Moere", 11 | "email": "jurgen.van.de.moere@gmail.com", 12 | "url": "http://www.jvandemo.com" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "http://github.com/jvandemo/contentful-webhook-server.git" 17 | }, 18 | "main": "index.js", 19 | "scripts": { 20 | "test": "mocha" 21 | }, 22 | "license": "MIT", 23 | "dependencies": { 24 | "basic-auth": "^1.0.0" 25 | }, 26 | "devDependencies": { 27 | "chai": "^2.2.0", 28 | "mocha": "^2.2.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var expect = chai.expect; 3 | 4 | var contentfulWebhookServerFactory = require('../index.js'); 5 | var server; 6 | 7 | beforeEach(function(){ 8 | server = contentfulWebhookServerFactory(); 9 | }); 10 | 11 | describe('contentful-webhook-server', function(){ 12 | 13 | it('should return a function', function(){ 14 | expect(contentfulWebhookServerFactory).to.be.a('function'); 15 | }); 16 | 17 | it('should return an object with a `listen` method', function(){ 18 | expect(server).to.be.an('object'); 19 | expect(server.listen).to.be.a('function'); 20 | }); 21 | 22 | }); 23 | --------------------------------------------------------------------------------