-
-
Notifications
You must be signed in to change notification settings - Fork 10.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Webhooks support for subscriber events #9230
Conversation
Is the plan for these to be used for things other than zapier? ie I can create hooks to a gateway I've written to get events from ghost? If so, are they going to be signed? It'd be great if they were signed like GitHub ones! https://developer.github.com/webhooks/securing/ |
@jloh this is for outgoing webhooks, I don't think the GitHub style signed requests make sense here - you already need to have a client_id/secret and access token because the webhook endpoints are behind the authentication. It's a basic implementation of http://resthooks.org. Right now we're only looking to do the basics that are needed to work with Zapier and will only work with a couple of events to start: "New post" events in Zapier will be triggered via polling instead. |
The receiver (zapier, other service etc) still need a way to verify that the webhook they receive is genuine and not from someone else. The GitHub method of signing hooks is mentioned on their security page http://resthooks.org/docs/security/ |
Signing may well be added later on, it's not something we're looking at for this early version. Security with Zapier is handled through the unique |
48da9dc
to
cd51797
Compare
core/server/api/index.js
Outdated
} else if (result.hasOwnProperty('webhooks')) { | ||
newObject = result.webhooks[0]; | ||
// TODO: this URL doesn't actually exist but is needed for a 201 response? | ||
location = utils.url.urlJoin(apiRoot, 'webhooks', newObject.id, '/'); |
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
core/server/models/webhook.js
Outdated
options = this.filterOptions(options, 'findAll'); | ||
|
||
return Webhook | ||
.query({where: {event: event}}) |
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
core/server/webhooks-events.js
Outdated
// webhook POST body should match GET request for same resource | ||
// TODO: is `options` needed here? | ||
var payload = { | ||
subscribers: [subscriber.toJSON()] |
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
core/server/webhooks-events.js
Outdated
} | ||
|
||
function triggerSubscriberRemoved(subscriber) { | ||
// TODO: this will be difficult to make generic, maybe we need to listen |
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
core/server/webhooks-events.js
Outdated
@@ -0,0 +1,79 @@ | |||
var _ = require('lodash'), |
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
core/server/models/webhook.js
Outdated
options = this.filterOptions(options, 'findAll'); | ||
|
||
return webhooksCollection | ||
.query({where: {event: event}}) |
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a very different pattern to any other code on the server side, so I can see that it's been a real struggle - this falls strongly in line with the conversation we had recently about having to invent everything all the time.
As the code works - it can be merged, no changes are really needed - however all of my comments are things we somehow need to codify into the codebase & surrounding documentation so that the way to solve these problems becomes obvious and not constant reinvention tests!
core/server/api/routes.js
Outdated
@@ -201,5 +201,9 @@ module.exports = function apiRoutes() { | |||
api.http(api.redirects.upload) | |||
); | |||
|
|||
// ## Webhooks (RESTHooks) | |||
apiRouter.post('/webhooks', mw.authenticatePrivate, api.http(api.webhooks.add)); | |||
apiRouter.delete('/webhooks/:id', mw.authenticatePrivate, api.http(api.webhooks.destroy)); |
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
core/server/api/webhooks.js
Outdated
} | ||
|
||
return models.Webhook.add(options.data.webhooks[0], _.omit(options, ['data'])).catch(function (error) { | ||
return Promise.reject(error); |
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
core/server/models/webhook.js
Outdated
options = this.filterOptions(options, 'findAll'); | ||
|
||
return webhooksCollection | ||
.query({where: {event: event}}) |
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
core/server/webhooks-events.js
Outdated
@@ -0,0 +1,79 @@ | |||
var _ = require('lodash'), |
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
core/server/webhooks-events.js
Outdated
events = require('./events'), | ||
logging = require('./logging'), | ||
models = require('./models'), | ||
https = require('https'), |
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
core/server/webhooks-events.js
Outdated
} | ||
|
||
function triggerSubscriberRemoved(subscriber) { | ||
// TODO: this will be difficult to make generic, maybe we need to listen |
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
core/server/webhooks-events.js
Outdated
}; | ||
|
||
// find relevant Webhooks | ||
models.Webhook.findAllByEvent('subscriber.added').then(function (result) { |
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
core/server/api/webhooks.js
Outdated
|
||
tasks = [ | ||
doQuery, | ||
_.partialRight(makeRequests, payload) |
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
* @returns {Object} options | ||
*/ | ||
function doQuery(options) { | ||
return models.Webhook.getByEventAndTarget(options.data.webhooks[0].event, options.data.webhooks[0].target_url, _.omit(options, ['data'])) |
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
No, they don't have any official limits so it's possible they will use a different format in the future. This is an example of one of the resthook URLs they're currently creating:
If we were to limit the composite index to 191 chars it needs to be split between the event name and the URL, so something like 30/161. That's on the assumption we never have our own event names > 30 chars (maybe 35 would be more reasonable?) and resthook URLs of other services don't reach double the length of Zapier's which I think is reasonable but we have no data to back that up. |
I've been looking at other webhook URL examples
Discord is getting pretty close to the suggested 161 or 156 limit for |
1e77fd3
to
918533b
Compare
I've reverted the db-level uniqueness constraint. Running a quick live test then I'll remove the WIP flag. |
docName = 'webhooks', | ||
webhooks; | ||
|
||
// TODO: Use the request util. Do we want retries here? |
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
core/server/api/webhooks.js
Outdated
// when a webhook responds with a 410 Gone response we should remove the hook | ||
if (err.status === 410) { | ||
logging.info('webhook.destroy (410 response)', event, targetUrl); | ||
return models.Webhook.destroy({id: webhookId}); |
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
return transacting.schema.hasTable(table) | ||
.then(function (exists) { | ||
if (exists) { | ||
logging.warn(message); |
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
core/server/models/webhook.js
Outdated
|
||
module.exports = { | ||
Webhook: ghostBookshelf.model('Webhook', Webhook), | ||
Webhooks: ghostBookshelf.collection('Webhook', Webhooks) |
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
@kirrg001 comments addressed |
// register listeners only for events that have webhooks | ||
function listen() { | ||
events.on('subscriber.added', _.partial(listener, 'subscriber.added')); | ||
events.on('subscriber.deleted', _.partial(listener, 'subscriber.deleted')); |
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
core/server/models/subscriber.js
Outdated
@@ -19,16 +21,16 @@ Subscriber = ghostBookshelf.Model.extend({ | |||
}; | |||
}, | |||
|
|||
onCreated: function onCreated(model) { | |||
model.emitChange('added'); | |||
onCreated: function onCreated(model, options) { |
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
// also known as "REST Hooks", see http://resthooks.org | ||
var Promise = require('bluebird'), | ||
_ = require('lodash'), | ||
https = require('https'), |
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
This comment was marked as abuse.
This comment was marked as abuse.
Sorry, something went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does not break anything. Should be good to go now.
no issue
Support for http://resthooks.org style webhooks that can be used with Zapier triggers. This can currently be used in two ways:
a) adding a webhook record to the DB manually
b) using the API with password auth and POSTing to /webhooks/ (this is private API so not documented)
target_url
field 🚨webhooks
table to store event names and target urlsPOST
andDELETE
endpoints for/webhooks/
subscribers.added
andsubscribers.deleted
events to trigger registered webhooks