require 'uri' require 'socket' require 'openssl' require 'json' module Houston Version = "0.0.1" APPLE_PRODUCTION_GATEWAY_URI = "apn://gateway.push.apple.com:2195" APPLE_PRODUCTION_FEEDBACK_URI = "apn://feedback.push.apple.com:2196" APPLE_DEVELOPMENT_GATEWAY_URI = "apn://gateway.sandbox.push.apple.com:2195" APPLE_DEVELOPMENT_FEEDBACK_URI = "apn://feedback.push.apple.com:2196" class Client attr_accessor :gateway_uri, :feedback_uri, :certificate, :passphrase def initialize @gateway_uri = ENV['APN_GATEWAY_URI'] @feedback_uri = ENV['APN_FEEDBACK_URI'] @certificate = ENV['APN_CERTIFICATE'] @passphrase = ENV['APN_CERTIFICATE_PASSPHRASE'] end def self.development client = self.new client.gateway_uri = APPLE_DEVELOPMENT_GATEWAY_URI client.feedback_uri = APPLE_DEVELOPMENT_FEEDBACK_URI client end def self.production client = self.new client.gateway_uri = APPLE_PRODUCTION_GATEWAY_URI client.feedback_uri = APPLE_PRODUCTION_FEEDBACK_URI client end def push(*notifications) Connection.open(connection_options_for_endpoint(:gateway)) do |connection, socket| notifications.each do |notification| next if notification.sent? connection.write(notification.message) notification.mark_as_sent! end end end def devices devices = [] Connection.open(connection_options_for_endpoint(:feedback)) do |connection, socket| while line = connection.read(38) feedback = line.unpack('N1n1H140') token = feedback[2].scan(/.{0,8}/).join(' ').strip devices << token if token end end devices end private def connection_options_for_endpoint(endpoint = :gateway) uri = case endpoint when :gateway then URI(@gateway_uri) when :feedback then URI(@feedback_uri) else raise ArgumentError end { certificate: @certificate, passphrase: @passphrase, host: uri.host, port: uri.port } end end class Notification attr_accessor :device, :alert, :badge, :sound, :custom_data attr_reader :sent_at def initialize(options = {}) @device = options.delete(:device) @alert = options.delete(:alert) @badge = options.delete(:badge) @sound = options.delete(:sound) @custom_data = options end def payload json = {}.merge(@custom_data || {}) json['aps'] = {} json['aps']['alert'] = @alert json['aps']['badge'] = @badge.to_i rescue 0 json['aps']['sound'] = @sound json end def message json = payload.to_json "\0\0 #{[@device.gsub(/[<\s>]/, '')].pack('H*')}\0#{json.length.chr}#{json}" end def mark_as_sent! @sent_at = Time.now end def sent? !!@sent_at end end class Connection class << self def open(options = {}) return unless block_given? [:certificate, :passphrase, :host, :port].each do |option| raise ArgumentError, "Missing connection parameter: #{option}" unless option end socket = TCPSocket.new(options[:host], options[:port]) context = OpenSSL::SSL::SSLContext.new context.key = OpenSSL::PKey::RSA.new(options[:certificate], options[:passphrase]) context.cert = OpenSSL::X509::Certificate.new(options[:certificate]) ssl = OpenSSL::SSL::SSLSocket.new(socket, context) ssl.sync = true ssl.connect yield ssl, socket ssl.close socket.close end end end end