<|start_filename|>package.json<|end_filename|>
{
  "name": "hubot-sns",
  "description": "Hubot script for receiving notifications from AWS Simple Notification Service",
  "version": "0.2.1",
  "homepage": "https://github.com/mdouglass/hubot-sns",
  "author": "<NAME> <<EMAIL>>",
  "license": "MIT",

  "keywords": [
    "hubot",
    "hubot-scripts",
    "aws",
    "sns"
  ],

  "repository": {
    "type": "git",
    "url": "https://github.com/mdouglass/hubot-sns.git"
  },

  "bugs": {
    "url": "https://github.com/mdouglass/hubot-sns/issues"
  },

  "dependencies": {
    "coffee-script": "~1.6"
  },

  "devDependencies": {
    "mocha": "*",
    "chai": "*",
    "sinon-chai": "*",
    "sinon": "*"
  },

  "main": "index.coffee",

  "scripts": {
    "test": "script/test"
  }
}


<|start_filename|>src/sns.coffee<|end_filename|>
# Description:
#   <description of the scripts functionality>
#
# Dependencies:
#   None
#
# Configuration:
#   HUBOT_SNS_URL
#
# Commands:
#   None
#
# URLs:
#   /hubot/sns
#
# Notes:
#   <optional notes required for the script>
#
# 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

module.exports = (robot) ->

  sns = new SNS robot

  robot.emit 'sns:ready', sns


<|start_filename|>src/support/sns-message-verify.coffee<|end_filename|>
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

module.exports = { verifySignature: verifySignature }
