class APNS::Connection

Attributes

error_handler[RW]

Public Class Methods

finalize(sock, ssl) click to toggle source
# File lib/apns/connection.rb, line 34
def self.finalize sock, ssl
  proc {
    ssl.close
    sock.close
  }
end
new(pem: , pass: nil, host: 'gateway.sandbox.push.apple.com', port: 2195, buffer_size: 4 * 1024) click to toggle source
# File lib/apns/connection.rb, line 12
def initialize(pem: ,
               pass: nil,
               host: 'gateway.sandbox.push.apple.com',
               port: 2195,
               buffer_size: 4 * 1024)
  @notifications = InfiniteArray.new(buffer_size: buffer_size)
  @pem =  pem
  @pass = pass
  @host = host
  @port = port

  # Our current strategy is to read errors emitted by the APNS in a separate
  # thread spawned when we open a new connection (read_errors method).
  # If this thread receives an error information from the @ssl, it turns on
  # the @lock, so that no direct write operation can be performed on the same
  # Connection instance.
  #
  @lock = Mutex.new

  @sock, @ssl = open_connection
  ObjectSpace.define_finalizer(self, self.class.finalize(@sock, @ssl))
end

Public Instance Methods

push(ns) click to toggle source
# File lib/apns/connection.rb, line 41
def push ns
  # The notification identifier is set to 4 bytes in the APNS protocol.
  # Thus, upon hitting this limit, read for failures and restart the counting again.
  if @notifications.size + ns.size > MAX_32_BIT - 10
    code, failed_id = read_failure_info(timeout: 3)
    if failed_id
      ns = @notifications.items_from(failed_id+1) + ns
      reopen_connection
      @error_handler.call(code, @notifications.item_at(failed_id))
    end

    @notifications.clear
  end

  ns.each{ |n|
    n.message_identifier = [@notifications.size].pack('N')
    @notifications.push(n)
  }
  @lock.synchronize{ write ns }
end

Private Instance Methods

inspect() click to toggle source

Override inspect since we do not want to print out the entire @notifications, whose size might be over tens of thousands

# File lib/apns/connection.rb, line 131
def inspect
  puts "#<#{self.class}:#{'0x%014x' % object_id} @pem=#{@pem} @pass=#{@pass} @host=#{@host} @port=#{@port} @error_handler=#{@error_handler}>"
end
open_connection() click to toggle source
# File lib/apns/connection.rb, line 97
def open_connection
  context      = OpenSSL::SSL::SSLContext.new
  context.cert = OpenSSL::X509::Certificate.new(File.read(@pem))
  context.key  = OpenSSL::PKey::RSA.new(File.read(@pem), @pass)

  sock         = TCPSocket.new(@host, @port)
  ssl          = OpenSSL::SSL::SSLSocket.new(sock,context)
  ssl.connect

  Thread.new {
    read_errors ssl
  }

  return sock, ssl
end
pack_notifications(notifications) click to toggle source
# File lib/apns/connection.rb, line 70
def pack_notifications notifications
  bytes = ''

  notifications.each do |n|
    # Each notification frame consists of
    # 1. (e.g. protocol version) 2 (unsigned char [1 byte])
    # 2. size of the full frame (unsigend int [4 byte], big endian)
    pn = n.packaged_notification
    bytes << ([2, pn.bytesize].pack('CN') + pn)
  end

  bytes
end
read_errors(ssl) click to toggle source
# File lib/apns/connection.rb, line 113
def read_errors ssl
  tuple = ssl.read(6)

  @lock.synchronize {
    _, code, failed_id = tuple.unpack("ccN")
    reopen_connection
    return unless failed_id # there's nothing we can do

    @error_handler.call(code, @notifications.item_at(failed_id))

    @notifications.delete_where_index_less_than(failed_id+1)
    ns = @notifications.items_from(failed_id+1)
    write ns
  }
end
read_failure_info(timeout:) click to toggle source
# File lib/apns/connection.rb, line 84
def read_failure_info(timeout:)
  tuple = Timeout::timeout(timeout){ @ssl.read(6) }
  _, code, failed_id = tuple.unpack("ccN")
  [code, failed_id]
rescue Timeout::Error
end
reopen_connection() click to toggle source
# File lib/apns/connection.rb, line 91
def reopen_connection
  @ssl.close
  @sock.close
  @sock, @ssl = open_connection
end
write(ns) click to toggle source
# File lib/apns/connection.rb, line 65
def write ns
  packed = pack_notifications(ns)
  @ssl.write(packed)
end