forked from thoukydides/homebridge-skybell
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathwebhooks.js
153 lines (129 loc) · 5.01 KB
/
webhooks.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
// Homebridge plugin for SkyBell HD video doorbells
// Copyright © 2017 Alexander Thoukydides
'use strict';
let http = require('http');
let url = require('url');
// Default options
const DEFAULT_OPTIONS = {
log: console.log,
// Base of all URL paths
basePath: '/homebridge-skybell/',
// An optional secret that must be included in all requests
secret: null,
// Maximum allowed payload size (in bytes)
maxPayload: 10 * 1000
};
// A webhooks server
module.exports = class Webhooks {
// Create a new webhooks server
constructor(port, options = {}) {
// Store the options, applying defaults for missing options
this.options = Object.assign({}, DEFAULT_OPTIONS, options);
// No clients initially
this.clients = {};
// Create a web server on the specified port
this.server = http.createServer(this.requestListener.bind(this));
this.server.on('error', (err) => {
this.options.log('Webhooks server error: ' + err);
});
this.server.listen(port, () => {
this.options.log('Webhooks listening on http://localhost:' + port
+ this.options.basePath + '...');
});
this.requestCount = 0;
}
// Handle a new incoming HTTP request
requestListener(request, response) {
let logPrefix = 'Webhook request #' + ++this.requestCount + ': ';
this.options.log(logPrefix + request.method + ' ' + request.url);
// Complete a request
let setStatusCode = statusCode => {
this.options.log(logPrefix + 'STATUS ' + statusCode
+ ' (' + http.STATUS_CODES[statusCode] + ')');
response.statusCode = statusCode;
response.end();
};
// Check whether the method and URL are acceptable
if (request.method != 'POST') {
this.options.log(logPrefix + 'Not using POST method');
return setStatusCode(405);
}
let parsedUrl = this.parseUrl(request.url);
if (!parsedUrl) {
this.options.log(logPrefix + 'No handler registered for URL');
return setStatusCode(404);
}
// Receive the body of the request
let body = '';
request.on('data', (chunk) => {
body += chunk;
if (this.options.maxPayload < body.length) {
this.options.log(logPrefix + 'Payload exceeds maximum size ('
+ this.options.maxPayload + ' bytes)');
request.destroy();
setStatusCode(413);
}
});
request.on('end', () => {
// Attempt to parse the body as JSON encoded
let parsedBody;
try {
parsedBody = JSON.parse(body);
}
catch (err) {
this.options.log(logPrefix + 'Body not JSON encoded: ' + err);
return setStatusCode(400);
}
// Check whether the request is authorised
if (!this.isAuthorised(parsedBody)) {
this.options.log(logPrefix + 'Secret not included in request');
return setStatusCode(403);
}
// Process the request
this.options.log(logPrefix + 'Dispatching '
+ JSON.stringify(parsedBody));
this.dispatchRequest(parsedUrl, parsedBody);
setStatusCode(200);
});
}
// Check whether a request is authorised
isAuthorised(body) {
return (!this.options.secret)
|| (body.secret == this.options.secret);
}
// Add a webhook for a specific URL path and body values
addHook(path, body, callback) {
let keyMap = key => key + "='" + body[key] + "'";
this.options.log('Adding webhook ' + this.options.basePath + path
+ ' for ' + Object.keys(body).map(keyMap).join(', '));
if (!this.clients[path]) this.clients[path] = [];
this.clients[path].push({
body: body,
callback: callback
});
}
// Parse and check a received URL
parseUrl(rawUrl) {
// Check the base of the path
let path = url.parse(rawUrl).pathname;
if (!path.startsWith(this.options.basePath)) {
return null;
}
// Check whether there are any clients for this path
let remain = path.substr(this.options.basePath.length);
return this.clients[remain];
}
// Dispatch a request to the appropriate client(s)
dispatchRequest(clients, body) {
let count = 0;
clients.forEach(client => {
let match = client.body;
if (Object.keys(match).every(key => body[key] == match[key])) {
++count;
client.callback(body);
}
});
this.options.log('Webhook request dispatched to ' + count
+ ((count == 1) ? ' client' : ' clients'));
}
};