github-webhook-handler 0.5.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.travis.yml +14 -0
- package/README.md +13 -3
- package/events.json +3 -1
- package/github-webhook-handler.d.ts +18 -0
- package/github-webhook-handler.js +89 -34
- package/package.json +10 -6
- package/test.js +205 -91
- package/.jshintrc +0 -59
- package/.npmignore +0 -1
package/.travis.yml
ADDED
package/README.md
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
# github-webhook-handler
|
|
2
2
|
|
|
3
|
-
[](https://travis-ci.org/rvagg/github-webhook-handler)
|
|
4
|
+
|
|
5
|
+
[](https://nodei.co/npm/github-webhook-handler/)
|
|
5
6
|
|
|
6
7
|
GitHub allows you to register **[Webhooks](https://developer.github.com/webhooks/)** for your repositories. Each time an event occurs on your repository, whether it be pushing code, filling issues or creating pull requests, the webhook address you register can be configured to be pinged with details.
|
|
7
8
|
|
|
8
9
|
This library is a small handler (or "middleware" if you must) for Node.js web servers that handles all the logic of receiving and verifying webhook requests from GitHub.
|
|
9
10
|
|
|
11
|
+
## Tips
|
|
12
|
+
|
|
13
|
+
In Github Webhooks settings, Content type must be `application/json`.
|
|
14
|
+
|
|
15
|
+
`application/x-www-form-urlencoded` won't work at present.
|
|
16
|
+
|
|
10
17
|
## Example
|
|
11
18
|
|
|
12
19
|
```js
|
|
@@ -40,12 +47,15 @@ handler.on('issues', function (event) {
|
|
|
40
47
|
})
|
|
41
48
|
```
|
|
42
49
|
|
|
50
|
+
for multiple handlers, please see [multiple-handlers-issue](https://github.com/rvagg/github-webhook-handler/pull/22#issuecomment-274301907).
|
|
51
|
+
|
|
43
52
|
## API
|
|
44
53
|
|
|
45
54
|
github-webhook-handler exports a single function, use this function to *create* a webhook handler by passing in an *options* object. Your options object should contain:
|
|
46
55
|
|
|
47
56
|
* `"path"`: the complete case sensitive path/route to match when looking at `req.url` for incoming requests. Any request not matching this path will cause the callback function to the handler to be called (sometimes called the `next` handler).
|
|
48
57
|
* `"secret"`: this is a hash key used for creating the SHA-1 HMAC signature of the JSON blob sent by GitHub. You should register the same secret key with GitHub. Any request not delivering a `X-Hub-Signature` that matches the signature generated using this key against the blob will be rejected and cause an `'error'` event (also the callback will be called with an `Error` object).
|
|
58
|
+
* `"events"`: an optional array of whitelisted event types (see: *events.json*). If defined, any incoming request whose `X-Github-Event` can't be found in the whitelist will be rejected. If only a single event type is acceptable, this option can also be a string.
|
|
49
59
|
|
|
50
60
|
The resulting **handler** function acts like a common "middleware" handler that you can insert into a processing chain. It takes `request`, `response`, and `callback` arguments. The `callback` is not called if the request is successfully handled, otherwise it is called either with an `Error` or no arguments.
|
|
51
61
|
|
|
@@ -66,4 +76,4 @@ Additionally, there is a special `'*'` even you can listen to in order to receiv
|
|
|
66
76
|
|
|
67
77
|
## License
|
|
68
78
|
|
|
69
|
-
**github-webhook-handler** is Copyright (c) 2014 Rod Vagg
|
|
79
|
+
**github-webhook-handler** is Copyright (c) 2014 Rod Vagg and licensed under the MIT License. All rights not explicitly granted in the MIT License are reserved. See the included [LICENSE.md](./LICENSE.md) file for more details.
|
package/events.json
CHANGED
|
@@ -10,13 +10,15 @@
|
|
|
10
10
|
"issue_comment": "Any time an Issue is commented on",
|
|
11
11
|
"issues": "Any time an Issue is opened or closed",
|
|
12
12
|
"member": "Any time a User is added as a collaborator to a non-Organization Repository",
|
|
13
|
+
"membership": "Any time a User is added or removed from a team. Organization hooks only.",
|
|
13
14
|
"page_build": "Any time a Pages site is built or results in a failed build",
|
|
14
15
|
"public": "Any time a Repository changes from private to public",
|
|
15
16
|
"pull_request_review_comment": "Any time a Commit is commented on while inside a Pull Request review (the Files Changed tab)",
|
|
16
17
|
"pull_request": "Any time a Pull Request is opened, closed, or synchronized (updated due to a new push in the branch that the pull request is tracking)",
|
|
17
18
|
"push": "Any git push to a Repository. This is the default event",
|
|
18
19
|
"release": "Any time a Release is published in the Repository",
|
|
20
|
+
"repository": "Any time a Repository is created. Organization hooks only.",
|
|
19
21
|
"status": "Any time a Repository has a status update from the API",
|
|
20
22
|
"team_add": "Any time a team is added or modified on a Repository",
|
|
21
23
|
"watchAny": "time a User watches the Repository"
|
|
22
|
-
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
///<reference types="node" />
|
|
2
|
+
|
|
3
|
+
import { IncomingMessage, ServerResponse } from "http";
|
|
4
|
+
import { EventEmitter } from "events";
|
|
5
|
+
|
|
6
|
+
interface CreateHandlerOptions {
|
|
7
|
+
path: string;
|
|
8
|
+
secret: string;
|
|
9
|
+
events?: string | string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface handler extends EventEmitter {
|
|
13
|
+
(req: IncomingMessage, res: ServerResponse, callback: (err: Error) => void): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare function createHandler(options: CreateHandlerOptions|CreateHandlerOptions[]): handler;
|
|
17
|
+
|
|
18
|
+
export = createHandler;
|
|
@@ -1,69 +1,124 @@
|
|
|
1
|
-
const EventEmitter = require('events')
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
, bl = require('bl')
|
|
5
|
-
, bufferEq = require('buffer-equal-constant-time')
|
|
1
|
+
const EventEmitter = require('events')
|
|
2
|
+
const crypto = require('crypto')
|
|
3
|
+
const bl = require('bl')
|
|
6
4
|
|
|
5
|
+
function findHandler (url, arr) {
|
|
6
|
+
if (!Array.isArray(arr)) {
|
|
7
|
+
return arr
|
|
8
|
+
}
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
let ret = arr[0]
|
|
11
|
+
for (let i = 0; i < arr.length; i++) {
|
|
12
|
+
if (url === arr[i].path) {
|
|
13
|
+
ret = arr[i]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
11
16
|
|
|
17
|
+
return ret
|
|
18
|
+
}
|
|
12
19
|
|
|
13
|
-
function
|
|
14
|
-
if (typeof options
|
|
20
|
+
function checkType (options) {
|
|
21
|
+
if (typeof options !== 'object') {
|
|
15
22
|
throw new TypeError('must provide an options object')
|
|
23
|
+
}
|
|
16
24
|
|
|
17
|
-
if (typeof options.path
|
|
25
|
+
if (typeof options.path !== 'string') {
|
|
18
26
|
throw new TypeError('must provide a \'path\' option')
|
|
27
|
+
}
|
|
19
28
|
|
|
20
|
-
if (typeof options.secret
|
|
29
|
+
if (typeof options.secret !== 'string') {
|
|
21
30
|
throw new TypeError('must provide a \'secret\' option')
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function create (initOptions) {
|
|
35
|
+
let options
|
|
36
|
+
// validate type of options
|
|
37
|
+
if (Array.isArray(initOptions)) {
|
|
38
|
+
for (let i = 0; i < initOptions.length; i++) {
|
|
39
|
+
checkType(initOptions[i])
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
checkType(initOptions)
|
|
43
|
+
}
|
|
22
44
|
|
|
23
|
-
// make it an EventEmitter
|
|
24
|
-
handler
|
|
45
|
+
// make it an EventEmitter
|
|
46
|
+
Object.setPrototypeOf(handler, EventEmitter.prototype)
|
|
25
47
|
EventEmitter.call(handler)
|
|
26
48
|
|
|
49
|
+
handler.sign = sign
|
|
50
|
+
handler.verify = verify
|
|
51
|
+
|
|
27
52
|
return handler
|
|
28
53
|
|
|
54
|
+
function sign (data) {
|
|
55
|
+
return `sha1=${crypto.createHmac('sha1', options.secret).update(data).digest('hex')}`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function verify (signature, data) {
|
|
59
|
+
const sig = Buffer.from(signature)
|
|
60
|
+
const signed = Buffer.from(sign(data))
|
|
61
|
+
if (sig.length !== signed.length) {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
return crypto.timingSafeEqual(sig, signed)
|
|
65
|
+
}
|
|
29
66
|
|
|
30
67
|
function handler (req, res, callback) {
|
|
31
|
-
|
|
68
|
+
let events
|
|
69
|
+
|
|
70
|
+
options = findHandler(req.url, initOptions)
|
|
71
|
+
|
|
72
|
+
if (typeof options.events === 'string' && options.events !== '*') {
|
|
73
|
+
events = [options.events]
|
|
74
|
+
} else if (Array.isArray(options.events) && options.events.indexOf('*') === -1) {
|
|
75
|
+
events = options.events
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (req.url !== options.path || req.method !== 'POST') {
|
|
32
79
|
return callback()
|
|
80
|
+
}
|
|
33
81
|
|
|
34
82
|
function hasError (msg) {
|
|
35
83
|
res.writeHead(400, { 'content-type': 'application/json' })
|
|
36
84
|
res.end(JSON.stringify({ error: msg }))
|
|
37
85
|
|
|
38
|
-
|
|
86
|
+
const err = new Error(msg)
|
|
39
87
|
|
|
40
88
|
handler.emit('error', err, req)
|
|
41
89
|
callback(err)
|
|
42
90
|
}
|
|
43
91
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
92
|
+
const sig = req.headers['x-hub-signature']
|
|
93
|
+
const event = req.headers['x-github-event']
|
|
94
|
+
const id = req.headers['x-github-delivery']
|
|
47
95
|
|
|
48
|
-
if (!sig)
|
|
96
|
+
if (!sig) {
|
|
49
97
|
return hasError('No X-Hub-Signature found on request')
|
|
98
|
+
}
|
|
50
99
|
|
|
51
|
-
if (!event)
|
|
100
|
+
if (!event) {
|
|
52
101
|
return hasError('No X-Github-Event found on request')
|
|
102
|
+
}
|
|
53
103
|
|
|
54
|
-
if (!id)
|
|
104
|
+
if (!id) {
|
|
55
105
|
return hasError('No X-Github-Delivery found on request')
|
|
106
|
+
}
|
|
56
107
|
|
|
57
|
-
|
|
108
|
+
if (events && events.indexOf(event) === -1) {
|
|
109
|
+
return hasError('X-Github-Event is not acceptable')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
req.pipe(bl((err, data) => {
|
|
58
113
|
if (err) {
|
|
59
114
|
return hasError(err.message)
|
|
60
115
|
}
|
|
61
116
|
|
|
62
|
-
|
|
63
|
-
var computedSig = new Buffer(signBlob(options.secret, data))
|
|
117
|
+
let obj
|
|
64
118
|
|
|
65
|
-
if (!
|
|
119
|
+
if (!verify(sig, data)) {
|
|
66
120
|
return hasError('X-Hub-Signature does not match blob signature')
|
|
121
|
+
}
|
|
67
122
|
|
|
68
123
|
try {
|
|
69
124
|
obj = JSON.parse(data.toString())
|
|
@@ -74,13 +129,14 @@ function create (options) {
|
|
|
74
129
|
res.writeHead(200, { 'content-type': 'application/json' })
|
|
75
130
|
res.end('{"ok":true}')
|
|
76
131
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
132
|
+
const emitData = {
|
|
133
|
+
event: event,
|
|
134
|
+
id: id,
|
|
135
|
+
payload: obj,
|
|
136
|
+
protocol: req.protocol,
|
|
137
|
+
host: req.headers.host,
|
|
138
|
+
url: req.url,
|
|
139
|
+
path: options.path
|
|
84
140
|
}
|
|
85
141
|
|
|
86
142
|
handler.emit(event, emitData)
|
|
@@ -89,5 +145,4 @@ function create (options) {
|
|
|
89
145
|
}
|
|
90
146
|
}
|
|
91
147
|
|
|
92
|
-
|
|
93
148
|
module.exports = create
|
package/package.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "github-webhook-handler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Web handler / middleware for processing GitHub Webhooks",
|
|
5
5
|
"main": "github-webhook-handler.js",
|
|
6
|
+
"types": "github-webhook-handler.d.ts",
|
|
6
7
|
"scripts": {
|
|
7
|
-
"
|
|
8
|
+
"lint": "standard *.js",
|
|
9
|
+
"test": "npm run lint && node test.js"
|
|
8
10
|
},
|
|
9
11
|
"keywords": [
|
|
10
12
|
"github",
|
|
@@ -17,11 +19,13 @@
|
|
|
17
19
|
},
|
|
18
20
|
"license": "MIT",
|
|
19
21
|
"dependencies": {
|
|
20
|
-
"bl": "~
|
|
21
|
-
"buffer-equal-constant-time": "~1.0.1"
|
|
22
|
+
"bl": "~4.0.0"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
|
-
"
|
|
25
|
-
"
|
|
25
|
+
"@types/node": "*",
|
|
26
|
+
"run-series": "~1.1.8",
|
|
27
|
+
"standard": "~14.3.1",
|
|
28
|
+
"tape": "~4.11.0",
|
|
29
|
+
"through2": "~3.0.1"
|
|
26
30
|
}
|
|
27
31
|
}
|
package/test.js
CHANGED
|
@@ -1,44 +1,41 @@
|
|
|
1
|
-
const test
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
const test = require('tape')
|
|
2
|
+
const crypto = require('crypto')
|
|
3
|
+
const handler = require('./')
|
|
4
|
+
const through2 = require('through2')
|
|
5
|
+
const series = require('run-series')
|
|
6
6
|
|
|
7
7
|
function signBlob (key, blob) {
|
|
8
|
-
return 'sha1
|
|
9
|
-
crypto.createHmac('sha1', key).update(blob).digest('hex')
|
|
8
|
+
return `sha1=${crypto.createHmac('sha1', key).update(blob).digest('hex')}`
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
function mkReq (url, method) {
|
|
12
|
+
const req = through2()
|
|
13
|
+
req.method = method || 'POST'
|
|
15
14
|
req.url = url
|
|
16
15
|
req.headers = {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
'x-hub-signature': 'bogus',
|
|
17
|
+
'x-github-event': 'bogus',
|
|
18
|
+
'x-github-delivery': 'bogus'
|
|
20
19
|
}
|
|
21
20
|
return req
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
|
|
25
23
|
function mkRes () {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
const res = {
|
|
25
|
+
writeHead: function (statusCode, headers) {
|
|
26
|
+
res.$statusCode = statusCode
|
|
27
|
+
res.$headers = headers
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
end: function (content) {
|
|
31
|
+
res.$end = content
|
|
32
|
+
}
|
|
35
33
|
}
|
|
36
34
|
|
|
37
35
|
return res
|
|
38
36
|
}
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
test('handler without full options throws', function (t) {
|
|
38
|
+
test('handler without full options throws', (t) => {
|
|
42
39
|
t.plan(4)
|
|
43
40
|
|
|
44
41
|
t.equal(typeof handler, 'function', 'handler exports a function')
|
|
@@ -50,44 +47,81 @@ test('handler without full options throws', function (t) {
|
|
|
50
47
|
t.throws(handler.bind(null, { path: '/' }), /must provide a 'secret' option/, 'throws if no secret option')
|
|
51
48
|
})
|
|
52
49
|
|
|
50
|
+
test('handler without full options throws in array', (t) => {
|
|
51
|
+
t.plan(2)
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
t.throws(handler.bind(null, [{}]), /must provide a 'path' option/, 'throws if no path option')
|
|
54
|
+
|
|
55
|
+
t.throws(handler.bind(null, [{ path: '/' }]), /must provide a 'secret' option/, 'throws if no secret option')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('handler ignores invalid urls', (t) => {
|
|
59
|
+
const options = { path: '/some/url', secret: 'bogus' }
|
|
60
|
+
const h = handler(options)
|
|
57
61
|
|
|
58
62
|
t.plan(6)
|
|
59
63
|
|
|
60
|
-
h(mkReq('/'), mkRes(),
|
|
64
|
+
h(mkReq('/'), mkRes(), (err) => {
|
|
61
65
|
t.error(err)
|
|
62
66
|
t.ok(true, 'request was ignored')
|
|
63
67
|
})
|
|
64
68
|
|
|
65
69
|
// near match
|
|
66
|
-
h(mkReq('/some/url/'), mkRes(),
|
|
70
|
+
h(mkReq('/some/url/'), mkRes(), (err) => {
|
|
67
71
|
t.error(err)
|
|
68
72
|
t.ok(true, 'request was ignored')
|
|
69
73
|
})
|
|
70
74
|
|
|
71
75
|
// partial match
|
|
72
|
-
h(mkReq('/some'), mkRes(),
|
|
76
|
+
h(mkReq('/some'), mkRes(), (err) => {
|
|
77
|
+
t.error(err)
|
|
78
|
+
t.ok(true, 'request was ignored')
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('handler ingores non-POST requests', (t) => {
|
|
83
|
+
const options = { path: '/some/url', secret: 'bogus' }
|
|
84
|
+
const h = handler(options)
|
|
85
|
+
|
|
86
|
+
t.plan(4)
|
|
87
|
+
|
|
88
|
+
h(mkReq('/some/url', 'GET'), mkRes(), (err) => {
|
|
89
|
+
t.error(err)
|
|
90
|
+
t.ok(true, 'request was ignored')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
h(mkReq('/some/url?test=param', 'GET'), mkRes(), (err) => {
|
|
73
94
|
t.error(err)
|
|
74
95
|
t.ok(true, 'request was ignored')
|
|
75
96
|
})
|
|
76
97
|
})
|
|
77
98
|
|
|
99
|
+
test('handler accepts valid urls', (t) => {
|
|
100
|
+
const options = { path: '/some/url', secret: 'bogus' }
|
|
101
|
+
const h = handler(options)
|
|
78
102
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
103
|
+
t.plan(1)
|
|
104
|
+
|
|
105
|
+
h(mkReq('/some/url'), mkRes(), (err) => {
|
|
106
|
+
t.error(err)
|
|
107
|
+
t.fail(false, 'should not call')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
setTimeout(t.ok.bind(t, true, 'done'))
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('handler accepts valid urls in Array', (t) => {
|
|
114
|
+
const options = [{ path: '/some/url', secret: 'bogus' }, { path: '/someOther/url', secret: 'bogus' }]
|
|
115
|
+
const h = handler(options)
|
|
82
116
|
|
|
83
117
|
t.plan(1)
|
|
84
118
|
|
|
85
|
-
h(mkReq('/some/url'), mkRes(),
|
|
119
|
+
h(mkReq('/some/url'), mkRes(), (err) => {
|
|
86
120
|
t.error(err)
|
|
87
121
|
t.fail(false, 'should not call')
|
|
88
122
|
})
|
|
89
123
|
|
|
90
|
-
h(mkReq('/
|
|
124
|
+
h(mkReq('/someOther/url'), mkRes(), (err) => {
|
|
91
125
|
t.error(err)
|
|
92
126
|
t.fail(false, 'should not call')
|
|
93
127
|
})
|
|
@@ -95,18 +129,75 @@ test('handler accepts valid urls', function (t) {
|
|
|
95
129
|
setTimeout(t.ok.bind(t, true, 'done'))
|
|
96
130
|
})
|
|
97
131
|
|
|
132
|
+
test('handler can reject events', (t) => {
|
|
133
|
+
const acceptableEvents = {
|
|
134
|
+
undefined: undefined,
|
|
135
|
+
'a string equal to the event': 'bogus',
|
|
136
|
+
'a string equal to *': '*',
|
|
137
|
+
'an array containing the event': ['bogus'],
|
|
138
|
+
'an array containing *': ['not-bogus', '*']
|
|
139
|
+
}
|
|
140
|
+
const unacceptableEvents = {
|
|
141
|
+
'a string not equal to the event or *': 'not-bogus',
|
|
142
|
+
'an array not containing the event or *': ['not-bogus']
|
|
143
|
+
}
|
|
144
|
+
const acceptable = Object.keys(acceptableEvents)
|
|
145
|
+
const unacceptable = Object.keys(unacceptableEvents)
|
|
146
|
+
const acceptableTests = acceptable.map((events) => {
|
|
147
|
+
return acceptableReq.bind(null, events)
|
|
148
|
+
})
|
|
149
|
+
const unacceptableTests = unacceptable.map((events) => {
|
|
150
|
+
return unacceptableReq.bind(null, events)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
t.plan(acceptable.length + unacceptable.length)
|
|
154
|
+
series(acceptableTests.concat(unacceptableTests))
|
|
155
|
+
|
|
156
|
+
function acceptableReq (events, callback) {
|
|
157
|
+
const h = handler({
|
|
158
|
+
path: '/some/url',
|
|
159
|
+
secret: 'bogus',
|
|
160
|
+
events: acceptableEvents[events]
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
h(mkReq('/some/url'), mkRes(), (err) => {
|
|
164
|
+
t.error(err)
|
|
165
|
+
t.fail(false, 'should not call')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
setTimeout(() => {
|
|
169
|
+
t.ok(true, 'accepted because options.events was ' + events)
|
|
170
|
+
callback()
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function unacceptableReq (events, callback) {
|
|
175
|
+
const h = handler({
|
|
176
|
+
path: '/some/url',
|
|
177
|
+
secret: 'bogus',
|
|
178
|
+
events: unacceptableEvents[events]
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
h.on('error', () => {})
|
|
182
|
+
|
|
183
|
+
h(mkReq('/some/url'), mkRes(), (err) => {
|
|
184
|
+
t.ok(err, 'rejected because options.events was ' + events)
|
|
185
|
+
callback()
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
})
|
|
98
189
|
|
|
99
190
|
// because we don't inherit in a traditional way
|
|
100
|
-
test('handler is an EventEmitter',
|
|
191
|
+
test('handler is an EventEmitter', (t) => {
|
|
101
192
|
t.plan(5)
|
|
102
193
|
|
|
103
|
-
|
|
194
|
+
const h = handler({ path: '/', secret: 'bogus' })
|
|
104
195
|
|
|
105
196
|
t.equal(typeof h.on, 'function', 'has h.on()')
|
|
106
197
|
t.equal(typeof h.emit, 'function', 'has h.emit()')
|
|
107
198
|
t.equal(typeof h.removeListener, 'function', 'has h.removeListener()')
|
|
108
199
|
|
|
109
|
-
h.on('ping',
|
|
200
|
+
h.on('ping', (pong) => {
|
|
110
201
|
t.equal(pong, 'pong', 'got event')
|
|
111
202
|
})
|
|
112
203
|
|
|
@@ -115,85 +206,110 @@ test('handler is an EventEmitter', function (t) {
|
|
|
115
206
|
t.throws(h.emit.bind(h, 'error', new Error('threw an error')), /threw an error/, 'acts like an EE')
|
|
116
207
|
})
|
|
117
208
|
|
|
118
|
-
|
|
119
|
-
test('handler accepts a signed blob', function (t) {
|
|
209
|
+
test('handler accepts a signed blob', (t) => {
|
|
120
210
|
t.plan(4)
|
|
121
211
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
212
|
+
const obj = { some: 'github', object: 'with', properties: true }
|
|
213
|
+
const json = JSON.stringify(obj)
|
|
214
|
+
const h = handler({ path: '/', secret: 'bogus' })
|
|
215
|
+
const req = mkReq('/')
|
|
216
|
+
const res = mkRes()
|
|
127
217
|
|
|
128
218
|
req.headers['x-hub-signature'] = signBlob('bogus', json)
|
|
129
|
-
req.headers['x-github-event']
|
|
219
|
+
req.headers['x-github-event'] = 'push'
|
|
130
220
|
|
|
131
|
-
h.on('push',
|
|
132
|
-
t.deepEqual(event, { event: 'push', id: 'bogus', payload: obj, url: '/', host: undefined, protocol: undefined })
|
|
221
|
+
h.on('push', (event) => {
|
|
222
|
+
t.deepEqual(event, { event: 'push', id: 'bogus', payload: obj, url: '/', host: undefined, protocol: undefined, path: '/' })
|
|
133
223
|
t.equal(res.$statusCode, 200, 'correct status code')
|
|
134
224
|
t.deepEqual(res.$headers, { 'content-type': 'application/json' })
|
|
135
225
|
t.equal(res.$end, '{"ok":true}', 'got correct content')
|
|
136
226
|
})
|
|
137
227
|
|
|
138
|
-
h(req, res,
|
|
228
|
+
h(req, res, (err) => {
|
|
139
229
|
t.error(err)
|
|
140
230
|
t.fail(true, 'should not get here!')
|
|
141
231
|
})
|
|
142
232
|
|
|
143
|
-
process.nextTick(
|
|
233
|
+
process.nextTick(() => {
|
|
144
234
|
req.end(json)
|
|
145
235
|
})
|
|
146
236
|
})
|
|
147
237
|
|
|
238
|
+
test('handler accepts multi blob in Array', (t) => {
|
|
239
|
+
t.plan(4)
|
|
240
|
+
|
|
241
|
+
const obj = { some: 'github', object: 'with', properties: true }
|
|
242
|
+
const json = JSON.stringify(obj)
|
|
243
|
+
const h = handler([{ path: '/', secret: 'bogus' }, { path: '/some/url', secret: 'bogus' }])
|
|
244
|
+
const req = mkReq('/some/url')
|
|
245
|
+
const res = mkRes()
|
|
246
|
+
req.headers['x-hub-signature'] = signBlob('bogus', json)
|
|
247
|
+
req.headers['x-github-event'] = 'push'
|
|
248
|
+
|
|
249
|
+
h.on('push', (event) => {
|
|
250
|
+
t.deepEqual(event, { event: 'push', id: 'bogus', payload: obj, url: '/some/url', host: undefined, protocol: undefined, path: '/some/url' })
|
|
251
|
+
t.equal(res.$statusCode, 200, 'correct status code')
|
|
252
|
+
t.deepEqual(res.$headers, { 'content-type': 'application/json' })
|
|
253
|
+
t.equal(res.$end, '{"ok":true}', 'got correct content')
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
h(req, res, (err) => {
|
|
257
|
+
t.error(err)
|
|
258
|
+
t.fail(true, 'should not get here!')
|
|
259
|
+
})
|
|
148
260
|
|
|
149
|
-
|
|
261
|
+
process.nextTick(() => {
|
|
262
|
+
req.end(json)
|
|
263
|
+
})
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
test('handler accepts a signed blob with alt event', (t) => {
|
|
150
267
|
t.plan(4)
|
|
151
268
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
269
|
+
const obj = { some: 'github', object: 'with', properties: true }
|
|
270
|
+
const json = JSON.stringify(obj)
|
|
271
|
+
const h = handler({ path: '/', secret: 'bogus' })
|
|
272
|
+
const req = mkReq('/')
|
|
273
|
+
const res = mkRes()
|
|
157
274
|
|
|
158
275
|
req.headers['x-hub-signature'] = signBlob('bogus', json)
|
|
159
|
-
req.headers['x-github-event']
|
|
276
|
+
req.headers['x-github-event'] = 'issue'
|
|
160
277
|
|
|
161
|
-
h.on('push',
|
|
278
|
+
h.on('push', (event) => {
|
|
162
279
|
t.fail(true, 'should not get here!')
|
|
163
280
|
})
|
|
164
281
|
|
|
165
|
-
h.on('issue',
|
|
166
|
-
t.deepEqual(event, { event: 'issue', id: 'bogus', payload: obj, url: '/', host: undefined, protocol: undefined })
|
|
282
|
+
h.on('issue', (event) => {
|
|
283
|
+
t.deepEqual(event, { event: 'issue', id: 'bogus', payload: obj, url: '/', host: undefined, protocol: undefined, path: '/' })
|
|
167
284
|
t.equal(res.$statusCode, 200, 'correct status code')
|
|
168
285
|
t.deepEqual(res.$headers, { 'content-type': 'application/json' })
|
|
169
286
|
t.equal(res.$end, '{"ok":true}', 'got correct content')
|
|
170
287
|
})
|
|
171
288
|
|
|
172
|
-
h(req, res,
|
|
289
|
+
h(req, res, (err) => {
|
|
173
290
|
t.error(err)
|
|
174
291
|
t.fail(true, 'should not get here!')
|
|
175
292
|
})
|
|
176
293
|
|
|
177
|
-
process.nextTick(
|
|
294
|
+
process.nextTick(() => {
|
|
178
295
|
req.end(json)
|
|
179
296
|
})
|
|
180
297
|
})
|
|
181
298
|
|
|
182
|
-
|
|
183
|
-
test('handler rejects a badly signed blob', function (t) {
|
|
299
|
+
test('handler rejects a badly signed blob', (t) => {
|
|
184
300
|
t.plan(6)
|
|
185
301
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
302
|
+
const obj = { some: 'github', object: 'with', properties: true }
|
|
303
|
+
const json = JSON.stringify(obj)
|
|
304
|
+
const h = handler({ path: '/', secret: 'bogus' })
|
|
305
|
+
const req = mkReq('/')
|
|
306
|
+
const res = mkRes()
|
|
191
307
|
|
|
192
308
|
req.headers['x-hub-signature'] = signBlob('bogus', json)
|
|
193
309
|
// break signage by a tiny bit
|
|
194
310
|
req.headers['x-hub-signature'] = '0' + req.headers['x-hub-signature'].substring(1)
|
|
195
311
|
|
|
196
|
-
h.on('error',
|
|
312
|
+
h.on('error', (err, _req) => {
|
|
197
313
|
t.ok(err, 'got an error')
|
|
198
314
|
t.strictEqual(_req, req, 'was given original request object')
|
|
199
315
|
t.equal(res.$statusCode, 400, 'correct status code')
|
|
@@ -201,56 +317,54 @@ test('handler rejects a badly signed blob', function (t) {
|
|
|
201
317
|
t.equal(res.$end, '{"error":"X-Hub-Signature does not match blob signature"}', 'got correct content')
|
|
202
318
|
})
|
|
203
319
|
|
|
204
|
-
h.on('push',
|
|
320
|
+
h.on('push', (event) => {
|
|
205
321
|
t.fail(true, 'should not get here!')
|
|
206
322
|
})
|
|
207
323
|
|
|
208
|
-
h(req, res,
|
|
324
|
+
h(req, res, (err) => {
|
|
209
325
|
t.ok(err, 'got error on callback')
|
|
210
326
|
})
|
|
211
327
|
|
|
212
|
-
process.nextTick(
|
|
328
|
+
process.nextTick(() => {
|
|
213
329
|
req.end(json)
|
|
214
330
|
})
|
|
215
331
|
})
|
|
216
332
|
|
|
217
|
-
test('handler responds on a bl error',
|
|
333
|
+
test('handler responds on a bl error', (t) => {
|
|
218
334
|
t.plan(4)
|
|
219
335
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
336
|
+
const obj = { some: 'github', object: 'with', properties: true }
|
|
337
|
+
const json = JSON.stringify(obj)
|
|
338
|
+
const h = handler({ path: '/', secret: 'bogus' })
|
|
339
|
+
const req = mkReq('/')
|
|
340
|
+
const res = mkRes()
|
|
225
341
|
|
|
226
342
|
req.headers['x-hub-signature'] = signBlob('bogus', json)
|
|
227
|
-
req.headers['x-github-event']
|
|
343
|
+
req.headers['x-github-event'] = 'issue'
|
|
228
344
|
|
|
229
|
-
h.on('push',
|
|
345
|
+
h.on('push', (event) => {
|
|
230
346
|
t.fail(true, 'should not get here!')
|
|
231
347
|
})
|
|
232
348
|
|
|
233
|
-
h.on('issue',
|
|
349
|
+
h.on('issue', (event) => {
|
|
234
350
|
t.fail(true, 'should never get here!')
|
|
235
351
|
})
|
|
236
352
|
|
|
237
|
-
h.on('error',
|
|
353
|
+
h.on('error', (err) => {
|
|
238
354
|
t.ok(err, 'got an error')
|
|
239
355
|
t.equal(res.$statusCode, 400, 'correct status code')
|
|
240
|
-
})
|
|
356
|
+
})
|
|
241
357
|
|
|
242
|
-
h(req, res,
|
|
358
|
+
h(req, res, (err) => {
|
|
243
359
|
t.ok(err)
|
|
244
360
|
})
|
|
245
361
|
|
|
246
|
-
|
|
247
|
-
res.end = function () {
|
|
362
|
+
res.end = () => {
|
|
248
363
|
t.equal(res.$statusCode, 400, 'correct status code')
|
|
249
364
|
}
|
|
250
365
|
|
|
251
366
|
req.write('{')
|
|
252
|
-
process.nextTick(
|
|
367
|
+
process.nextTick(() => {
|
|
253
368
|
req.emit('error', new Error('simulated explosion'))
|
|
254
|
-
})
|
|
255
|
-
|
|
369
|
+
})
|
|
256
370
|
})
|
package/.jshintrc
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"predef": [ ]
|
|
3
|
-
, "bitwise": false
|
|
4
|
-
, "camelcase": false
|
|
5
|
-
, "curly": false
|
|
6
|
-
, "eqeqeq": false
|
|
7
|
-
, "forin": false
|
|
8
|
-
, "immed": false
|
|
9
|
-
, "latedef": false
|
|
10
|
-
, "noarg": true
|
|
11
|
-
, "noempty": true
|
|
12
|
-
, "nonew": true
|
|
13
|
-
, "plusplus": false
|
|
14
|
-
, "quotmark": true
|
|
15
|
-
, "regexp": false
|
|
16
|
-
, "undef": true
|
|
17
|
-
, "unused": true
|
|
18
|
-
, "strict": false
|
|
19
|
-
, "trailing": true
|
|
20
|
-
, "maxlen": 120
|
|
21
|
-
, "asi": true
|
|
22
|
-
, "boss": true
|
|
23
|
-
, "debug": true
|
|
24
|
-
, "eqnull": true
|
|
25
|
-
, "esnext": true
|
|
26
|
-
, "evil": true
|
|
27
|
-
, "expr": true
|
|
28
|
-
, "funcscope": false
|
|
29
|
-
, "globalstrict": false
|
|
30
|
-
, "iterator": false
|
|
31
|
-
, "lastsemic": true
|
|
32
|
-
, "laxbreak": true
|
|
33
|
-
, "laxcomma": true
|
|
34
|
-
, "loopfunc": true
|
|
35
|
-
, "multistr": false
|
|
36
|
-
, "onecase": false
|
|
37
|
-
, "proto": false
|
|
38
|
-
, "regexdash": false
|
|
39
|
-
, "scripturl": true
|
|
40
|
-
, "smarttabs": false
|
|
41
|
-
, "shadow": false
|
|
42
|
-
, "sub": true
|
|
43
|
-
, "supernew": false
|
|
44
|
-
, "validthis": true
|
|
45
|
-
, "browser": true
|
|
46
|
-
, "couch": false
|
|
47
|
-
, "devel": false
|
|
48
|
-
, "dojo": false
|
|
49
|
-
, "mootools": false
|
|
50
|
-
, "node": true
|
|
51
|
-
, "nonstandard": true
|
|
52
|
-
, "prototypejs": false
|
|
53
|
-
, "rhino": false
|
|
54
|
-
, "worker": true
|
|
55
|
-
, "wsh": false
|
|
56
|
-
, "nomen": false
|
|
57
|
-
, "onevar": false
|
|
58
|
-
, "passfail": false
|
|
59
|
-
}
|
package/.npmignore
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
node_modules
|