class Heroic::SNS::Endpoint

Heroic::SNS::Endpoint is Rack middleware which intercepts messages from Amazon's Simple Notification Service (SNS). It makes the parsed and verified message available to your application in the Rack environment under the 'sns.message' key. If an error occurred during message handling, the error is available in the Rack environment in the 'sns.error' key.

Endpoint is to be initialized with a hash of options. It understands three different options:

:topic (or :topics) specifies a filter that defines what SNS topics are handled by this endpoint (“on-topic”). You can supply any of the following:

You must specify a topic filter. Use Proc.new{true} if you insist on indiscriminately accepting all notifications.

:auto_confirm determines how SubscriptionConfirmation messages are handled.

:auto_resubscribe affects how on-topic UnsubscribeConfirmation messages are handled.

You can install this in your config.ru:

use Heroic::SNS::Endpoint, :topics => /whatever/

For Rails, you can also install it in /config/initializers/sns_endpoint.rb:

Rails.application.config.middleware.use Heroic::SNS::Endpoint, :topic => ...

Constants

DEFAULT_OPTIONS
OK_RESPONSE

Public Class Methods

new(app, opt = {}) click to toggle source
# File lib/heroic/sns/endpoint.rb, line 56
def initialize(app, opt = {})
  @app = app
  options = DEFAULT_OPTIONS.merge(opt)
  @auto_confirm = options[:auto_confirm]
  @auto_resubscribe = options[:auto_resubscribe]
  if 1 < [:topic, :topics].count { |k| options.has_key?(k) }
    raise ArgumentError.new("supply zero or one of :topic, :topics")
  end
  @topic_filter = begin
    case a = options[:topic] || options[:topics]
    when String then Proc.new { |t| a == t }
    when Regexp then Proc.new { |t| a.match(t) }
    when Proc then a
    when Array
      unless a.all? { |e| e.is_a? String }
        raise ArgumentError.new("topic array must be strings")
      end
      Proc.new { |t| a.include?(t) }
    when nil
      raise ArgumentError.new("must specify a topic filter!")
    else
      raise ArgumentError.new("can't use topic filter of type #{a.class}")
    end
  end
end

Public Instance Methods

call(env) click to toggle source
# File lib/heroic/sns/endpoint.rb, line 82
def call(env)
  if topic_arn = env['HTTP_X_AMZ_SNS_TOPIC_ARN']
    if @topic_filter.call(topic_arn)
      call_on_topic(env)
    else
      call_off_topic(env)
    end
  else
    @app.call(env)
  end
end

Private Instance Methods

call_off_topic(env) click to toggle source

Default behavior for “off-topic” messages. Subscription and unsubscribe confirmations are simply ignored. Notifications, however, indicate that we are subscribed to a topic we don't know how to deal with. In this case, we automatically unsubscribe (if the message is authentic).

# File lib/heroic/sns/endpoint.rb, line 137
def call_off_topic(env)
  if env['HTTP_X_AMZ_SNS_MESSAGE_TYPE'] == 'Notification'
    begin
      message = Message.new(env['rack.input'].read)
      message.verify!
      URI.parse(message.unsubscribe_url).open
    rescue => e
      raise Error.new("error handling off-topic notification: #{e.message}", message)
    end
  end
  OK_RESPONSE
end
call_on_topic(env) click to toggle source

Behavior for “on-topic” messages. Notifications are always passed along to the app. Confirmations are passed along only if their respective option is nil. If true, the subscription is confirmed; if false, it is simply ignored.

# File lib/heroic/sns/endpoint.rb, line 110
def call_on_topic(env)
  begin
    message = Message.new(env['rack.input'].read)
    env['rack.input'].rewind
    check_headers!(message, env)
    message.verify!
    case message.type
    when 'SubscriptionConfirmation'
      URI.parse(message.subscribe_url).open if @auto_confirm
      return OK_RESPONSE unless @auto_confirm.nil?
    when 'UnsubscribeConfirmation'
      URI.parse(message.subscribe_url).open if @auto_resubscribe
      return OK_RESPONSE unless @auto_resubscribe.nil?
    end
    env['sns.message'] = message
  rescue OpenURI::HTTPError => e
    env['sns.error'] = Error.new("unable to subscribe: #{e.message}; URL: #{message.subscribe_url}", message)
  rescue Error => e
    env['sns.error'] = e
  end
  @app.call(env)
end
check_headers!(message, env) click to toggle source

Confirms that values specified in HTTP headers match those in the message itself.

# File lib/heroic/sns/endpoint.rb, line 100
def check_headers!(message, env)
  h = env.values_at 'HTTP_X_AMZ_SNS_MESSAGE_TYPE', 'HTTP_X_AMZ_SNS_MESSAGE_ID', 'HTTP_X_AMZ_SNS_TOPIC_ARN'
  m = message.type, message.id, message.topic_arn
  raise Error.new("message does not match HTTP headers", message) unless h == m
end