Skip to content

Commit

Permalink
Merge pull request #47 from toadkicker/issue_39
Browse files Browse the repository at this point in the history
Issue #39 - Add SNS Support
  • Loading branch information
yoheimuta authored Aug 19, 2016
2 parents 76a628c + 0fb85ef commit aac5bdd
Show file tree
Hide file tree
Showing 10 changed files with 303 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules
.DS_Store*
.idea
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ hubot s3 ls - Displays all S3 buckets
hubot s3 ls --bucket_name=[bucket-name] - Displays all objects
hubot s3 ls --bucket_name=[bucket-name] --prefix=[prefix] - Displays all objects with prefix
hubot s3 ls --bucket_name=[bucket-name] --prefix=[prefix] --marker=[marker] - Displays all objects with prefix from marker
hubot sns list topics
hubot sns list subscriptions
hubot sns list subscription in [topicArn]
hubot sns publish [message] to [topicArn]
```

## Configurations
Expand Down Expand Up @@ -307,6 +311,39 @@ images/001c03788ee31167872d38ce09493a4deb1cbe11728a762065ee1a5acfd1404b/
...
```

### SNS

#### Configuring

In addition to administration of SNS, hubot-aws can also receive push notifications.

* Create a new SNS topic
* Create a subscription and choose HTTP(S). The default configuration is http://<huboturl>:8080/hubot/sns
* The URL can be set using HUBOT_SNS_URL
* Set HUBOT_SNS_JID to hubot's jabber ID

You should see the subscription ID change from PendingConfirmation to a valid subscription id.

#### Receiving Messages

Use the Subject property to set which room the message should be delivered in, usually its JID. Messages can be raw or JSON format, but JSON is preferred.
If you are using [hubot-hipchat](https://github.com/hipchat/hubot-hipchat) use the plain old room name for the Subject.
The Subject property does not accept multiple rooms. Additionally the Messages does not natively process html templates, but one could implement it.

#### Sending Messages

`hubot-aws` does not send the message to a chat room. This is due to the myriad of adapters and clients hubot supports.
To process a message and send it to your chat room, hook into the `sns:notification` event.

```
# Use this snippet in your own scripts to send messages to the chat room from SNS
robot.on 'sns:notification', (message) ->
#if using hipchat the channelID function converts the common name to the room JID
robot.messageRoom sns.channelID(message.subject), message.message
#if using another adapter
robot.messageRoom message.subject, message.message
```

## Recommended Usage

### Use `--dry-run`
Expand Down
23 changes: 23 additions & 0 deletions scripts/sns/list_subscriptions.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Description:
# List sns subscriptions
#
# Commands:
# hubot sns list subscriptions

module.exports = (robot) ->
robot.respond /sns list subscriptions$/i, (msg) ->

msg.send "Fetching ..."

aws = require('../../aws.coffee').aws()
sns = new aws.SNS()

sns.listSubscriptions {}, (err, response) ->
if err
msg.send "Error: #{err}"
else
response.Subscriptions.forEach (subscription) ->
labels = Object.keys(subscription)
labels.forEach (label) ->
msg.send(label + " " + subscription[label])
msg.send "____________________________________"
26 changes: 26 additions & 0 deletions scripts/sns/list_subscriptions_by_topic.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Description:
# List sns subscriptions by topic
#
# Commands:
# hubot sns list subscriptions in topic arn:aws...

module.exports = (robot) ->

robot.respond /sns list subscriptions in (.*)$/i, (msg) ->

topic = msg.match[1]

msg.send "Fetching subscriptions for " + topic

aws = require('../../aws.coffee').aws()
sns = new aws.SNS()

sns.listSubscriptionsByTopic {TopicArn: topic}, (err, response) ->
if err
msg.send "Error: #{err}"
else
response.Subscriptions.forEach (subscription) ->
labels = Object.keys(subscription)
labels.forEach (label) ->
msg.send(label + " " + subscription[label])
msg.send "____________________________________"
23 changes: 23 additions & 0 deletions scripts/sns/list_topics.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Description:
# List sns topics
#
# Commands:
# hubot sns list topics

moment = require 'moment'
tsv = require 'tsv'

module.exports = (robot) ->
robot.respond /sns list topics$/i, (msg) ->

msg.send "Fetching ..."

aws = require('../../aws.coffee').aws()
sns = new aws.SNS()

sns.listTopics {}, (err, response) ->
if err
msg.send "Error: #{err}"
else
response.Topics.forEach (topic) ->
msg.send(topic.TopicArn)
25 changes: 25 additions & 0 deletions scripts/sns/publish.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Description:
# Publish a message to SNS
#
# Commands:
# hubot sns publish {message} to {message}

module.exports = (robot) ->
robot.respond /sns publish (.*) to (.*)/i, (msg) ->
topicArn = msg.match[2]
message = msg.match[1]
subject = "Hubot SNS Published"
msg.send('Publishing to ' + msg.match[2])

aws = require('../../aws.coffee').aws()
sns = new aws.SNS()
params = {
TopicArn: topicArn,
Message: message,
Subject: subject
}
sns.publish params, (err, response) ->
if err
msg.reply "Error: #{err}"
else
msg.reply JSON.stringify(response)
104 changes: 104 additions & 0 deletions scripts/sns/sns.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Description:
# Allows for Hubot to recieve push notifications from SNS
#
# Dependencies:
# None
#
# Configuration:
# HUBOT_SNS_URL - the URL you want AWS SNS to POST messages
# HUBOT_HIPCHAT_JID - the jabber id of the hubot
#
# Commands:
# None
#
# URLs:
# /hubot/sns
#
# Notes:
# Use this snippet in your own scripts to send messages to the chat room from SNS:
#
# robot.on 'sns:notification', (message) ->
# robot.messageRoom sns.channelID(message.subject), message.message
#
# Author:
# mdouglass
{inspect} = require 'util'
{ verifySignature } = require './support/sns_message_verify'

Options =
url: process.env.HUBOT_SNS_URL or '/hubot/sns'

class SNS
constructor: (robot) ->
@robot = robot

@robot.router.post Options.url, (req, res) => @onMessage req, res

onMessage: (req, res) ->
chunks = []

req.on 'data', (chunk) ->
chunks.push(chunk)

req.on 'end', =>
req.body = JSON.parse(chunks.join(''))
verifySignature req.body, (error) =>
if error
@robot.logger.warning "#{error}\n#{inspect req.body}"
@fail req, res
else
@process req, res

fail: (req, res) ->
res.writeHead(500)
res.end('Internal Error')

process: (req, res) ->
res.writeHead(200)
res.end('OK')

@robot.logger.debug "SNS Message: #{inspect req.body}"
if req.body.Type == 'SubscriptionConfirmation'
@confirmSubscribe req.body
else if req.body.Type == 'UnsubscribeConfirmation'
@confirmUnsubscribe
else if req.body.Type == 'Notification'
@notify req.body

confirmSubscribe: (msg) ->
@robot.emit 'sns:subscribe:request', msg

@robot.http(msg.SubscribeURL).get() (err, res, body) =>
if not err
@robot.emit 'sns:subscribe:success', msg
else
@robot.emit 'sns:subscribe:failure', err
return

confirmUnsubscribe: (msg) ->
@robot.emit 'sns:unsubscribe:request', msg
@robot.emit 'sns:unsubscribe:success', msg

notify: (msg) ->
message =
topic: msg.TopicArn.split(':').reverse()[0]
topicArn: msg.TopicArn
subject: msg.Subject
message: msg.Message
messageId: msg.MessageId

@robot.emit 'sns:notification', message
@robot.emit 'sns:notification:' + message.topic, message

# Given a room name, create a fully qualified room JID
# This is specific to hipchat.
channelID: (name) ->
if process.env.HUBOT_HIPCHAT_JID
temp = name.toLowerCase().replace(/\s/g, "_")
"#{process.env.HUBOT_HIPCHAT_JID.split("_")[0]}_#{temp}@conf.hipchat.com"
else
name.toLowerCase().replace(/\s/g, "_")

module.exports = (robot) ->
sns = new SNS robot
robot.emit 'sns:ready', sns
4 changes: 4 additions & 0 deletions scripts/sns/support/example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"default": "Hi! I learned how to get push notifications!",
"http": "Hi! I learned how to get push notifications!"
}
59 changes: 59 additions & 0 deletions scripts/sns/support/sns_message_verify.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
https = require('https')
crypto = require('crypto')
{inspect} = require('util')

certificateCache = {}

downloadCertificate = (url, cb) ->
if url is undefined
return cb new Error("Certificate URL not specified")

if url in certificateCache
return cb null, certificateCache[url]

req = https.get url, (res) ->
chunks = []

res.on 'data', (chunk) ->
chunks.push(chunk)
res.on 'end', ->
certificateCache[url] = chunks.join('')
return cb null, certificateCache[url]

req.on 'error', (error) ->
return cb new Error('Certificate download failed: ' + error)

signatureStringOrder =
'Notification': ['Message', 'MessageId', 'Subject', 'Timestamp', 'TopicArn', 'Type'],
'SubscriptionConfirmation': ['Message', 'MessageId', 'SubscribeURL', 'Timestamp', 'Token', 'TopicArn', 'Type'],
'UnsubscribeConfirmation': ['Message', 'MessageId', 'SubscribeURL', 'Timestamp', 'Token', 'TopicArn', 'Type']

createSignatureString = (msg) ->
chunks = []
for field in signatureStringOrder[msg.Type]
if field of msg
chunks.push field
chunks.push msg[field]
return chunks.join('\n') + '\n'

verifySignature = (msg, cb) ->
if msg.SignatureVersion isnt '1'
return cb new Error("SignatureVersion '#{msg.SignatureVersion}' not supported.")

downloadCertificate msg.SigningCertURL, (error, pem) ->
if error
return cb error

signatureString = createSignatureString msg

try
verifier = crypto.createVerify('RSA-SHA1')
verifier.update(signatureString, 'utf8')
if not verifier.verify(pem, msg.Signature, 'base64')
return cb new Error('Signature verification failed')
catch error
return cb new Error('Signature verification failed: ' + error)

return cb null

exports.verifySignature = verifySignature
2 changes: 1 addition & 1 deletion test/test_load.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@ describe 'hubot-aws', ->
false unless fs.existsSync category_path

for file in fs.readdirSync(category_path)
script = require path.resolve category_path, file
script = require path.resolve category_path, file unless file == 'support'
assert script != undefined

0 comments on commit aac5bdd

Please sign in to comment.