Skip to content
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

Merged
merged 25 commits into from
Nov 21, 2017

Conversation

kevinansfield
Copy link
Member

@kevinansfield kevinansfield commented Nov 9, 2017

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)

⚠️ only https URLs are supported in the webhook target_url field 🚨

  • add webhooks table to store event names and target urls
  • add POST and DELETE endpoints for /webhooks/
  • configure subscribers.added and subscribers.deleted events to trigger registered webhooks

@kirrg001
Copy link
Contributor

kirrg001 commented Nov 9, 2017

Note: We should merge and release #9199 before this. #9199 for Tuesday then 👍

@jloh
Copy link
Member

jloh commented Nov 9, 2017

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/

@kevinansfield
Copy link
Member Author

kevinansfield commented Nov 9, 2017

@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: subscriber.create and subscriber.delete

"New post" events in Zapier will be triggered via polling instead.

@jloh
Copy link
Member

jloh commented Nov 9, 2017

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/

@kevinansfield
Copy link
Member Author

kevinansfield commented Nov 9, 2017

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 target_url and UUID of the webhook in Ghost - to exploit the receiver in Zapier someone would have had to access your database or sniff HTTP traffic on the original webhook creation request if you are hosting your blog on HTTP instead of HTTPS.

@kevinansfield kevinansfield force-pushed the resthooks branch 3 times, most recently from 48da9dc to cd51797 Compare November 16, 2017 17:24
} 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.

This comment was marked as abuse.

This comment was marked as abuse.

This comment was marked as abuse.

This comment was marked as abuse.

options = this.filterOptions(options, 'findAll');

return Webhook
.query({where: {event: event}})

This comment was marked as abuse.

This comment was marked as abuse.

// 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.

}

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.

@@ -0,0 +1,79 @@
var _ = require('lodash'),

This comment was marked as abuse.

This comment was marked as abuse.

options = this.filterOptions(options, 'findAll');

return webhooksCollection
.query({where: {event: event}})

This comment was marked as abuse.

This comment was marked as abuse.

Copy link
Member

@ErisDS ErisDS left a 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!

@@ -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.

}

return models.Webhook.add(options.data.webhooks[0], _.omit(options, ['data'])).catch(function (error) {
return Promise.reject(error);

This comment was marked as abuse.

options = this.filterOptions(options, 'findAll');

return webhooksCollection
.query({where: {event: event}})

This comment was marked as abuse.

@@ -0,0 +1,79 @@
var _ = require('lodash'),

This comment was marked as abuse.

events = require('./events'),
logging = require('./logging'),
models = require('./models'),
https = require('https'),

This comment was marked as abuse.

}

function triggerSubscriberRemoved(subscriber) {
// TODO: this will be difficult to make generic, maybe we need to listen

This comment was marked as abuse.

};

// find relevant Webhooks
models.Webhook.findAllByEvent('subscriber.added').then(function (result) {

This comment was marked as abuse.

@kevinansfield
Copy link
Member Author

@ErisDS I've attempted a refactor to make this more Ghost-like in 1e75a11, feel free to shoot that approach down if I've gone the wrong way, I'm certainly not attached to it! 😄


tasks = [
doQuery,
_.partialRight(makeRequests, payload)

This comment was marked as abuse.

* @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.

This comment was marked as abuse.

This comment was marked as abuse.

@kevinansfield
Copy link
Member Author

Is that their official limit?

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:

https://hooks.zapier.com/hooks/standard/2665800/ae8965c1740e4db78571dfb486f7ccd0/
(81 chars)

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.

@kevinansfield
Copy link
Member Author

I've been looking at other webhook URL examples

https://discordapp.com/api/webhooks/380692408364433418/mGLHSRyEoUaTvY91Te16WOT8Obn-BrJoiTNoxeUqhb6klKERb9xaZkUBYC5AfduwYCCy
(123 chars)

https://hooks.slack.com/services/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
(77 chars)

Discord is getting pretty close to the suggested 161 or 156 limit for target_url. For future compatibility I'd probably swing towards dropping the db constraint and reverting to the in-app validation when adding so there's no real chance of running into length limitations.

@kevinansfield
Copy link
Member Author

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.

@kevinansfield kevinansfield changed the title [WIP] Webhooks/REST Hooks support ✨ Support for subscribers Zapier integration Nov 21, 2017
@kevinansfield kevinansfield changed the title ✨ Support for subscribers Zapier integration ✨ Webhooks support for subscriber events Nov 21, 2017
// 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.

return transacting.schema.hasTable(table)
.then(function (exists) {
if (exists) {
logging.warn(message);

This comment was marked as abuse.


module.exports = {
Webhook: ghostBookshelf.model('Webhook', Webhook),
Webhooks: ghostBookshelf.collection('Webhook', Webhooks)

This comment was marked as abuse.

@kevinansfield
Copy link
Member Author

@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.

@@ -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.

// 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.

This comment was marked as abuse.

This comment was marked as abuse.

This comment was marked as abuse.

This comment was marked as abuse.

This comment was marked as abuse.

This comment was marked as abuse.

@kevinansfield kevinansfield changed the title ✨ Webhooks support for subscriber events Webhooks support for subscriber events Nov 21, 2017
Copy link
Contributor

@kirrg001 kirrg001 left a 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.

@kevinansfield kevinansfield merged commit bffb3db into TryGhost:master Nov 21, 2017
@kevinansfield kevinansfield deleted the resthooks branch November 21, 2017 15:43
kirrg001 added a commit to kirrg001/Ghost that referenced this pull request Dec 12, 2017
kevinansfield pushed a commit that referenced this pull request Dec 12, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants