class Rpush::Daemon::TcpConnection

Constants

KEEPALIVE_IDLE
KEEPALIVE_INTERVAL
KEEPALIVE_MAX_FAIL_PROBES
OSX_TCP_KEEPALIVE
TCP_ERRORS

Attributes

host[R]
last_touch[RW]
port[R]

Public Class Methods

idle_period() click to toggle source
# File lib/rpush/daemon/tcp_connection.rb, line 18
def self.idle_period
  30.minutes
end
new(app, host, port) click to toggle source
# File lib/rpush/daemon/tcp_connection.rb, line 22
def initialize(app, host, port)
  @app = app
  @host = host
  @port = port
  @certificate = app.certificate
  @password = app.password
  @connected = false
  @connection_callbacks = []
  touch
end

Public Instance Methods

close() click to toggle source
# File lib/rpush/daemon/tcp_connection.rb, line 54
def close
  @ssl_socket.close if @ssl_socket
  @tcp_socket.close if @tcp_socket
rescue IOError # rubocop:disable HandleExceptions
end
connect() click to toggle source
# File lib/rpush/daemon/tcp_connection.rb, line 38
def connect
  @ssl_context = setup_ssl_context
  @tcp_socket, @ssl_socket = connect_socket
  @connected = true

  @connection_callbacks.each do |blk|
    begin
      blk.call
    rescue StandardError => e
      log_error(e)
    end
  end

  @connection_callbacks.clear
end
on_connect(&blk) click to toggle source
# File lib/rpush/daemon/tcp_connection.rb, line 33
def on_connect(&blk)
  raise 'already connected' if @connected
  @connection_callbacks << blk
end
read(num_bytes) click to toggle source
# File lib/rpush/daemon/tcp_connection.rb, line 60
def read(num_bytes)
  @ssl_socket.read(num_bytes) if @ssl_socket
end
reconnect() click to toggle source
# File lib/rpush/daemon/tcp_connection.rb, line 100
def reconnect
  close
  @tcp_socket, @ssl_socket = connect_socket
end
reconnect_with_rescue() click to toggle source
# File lib/rpush/daemon/tcp_connection.rb, line 94
def reconnect_with_rescue
  reconnect
rescue StandardError => e
  log_error(e)
end
select(timeout) click to toggle source
# File lib/rpush/daemon/tcp_connection.rb, line 64
def select(timeout)
  IO.select([@ssl_socket], nil, nil, timeout) if @ssl_socket
end
write(data) click to toggle source
# File lib/rpush/daemon/tcp_connection.rb, line 68
def write(data)
  connect unless @connected
  reconnect_idle if idle_period_exceeded?

  retry_count = 0

  begin
    write_data(data)
  rescue *TCP_ERRORS => e
    retry_count += 1

    if retry_count == 1
      log_error("Lost connection to #{@host}:#{@port} (#{e.class.name}, #{e.message}), reconnecting...")
      reflect(:tcp_connection_lost, @app, e)
    end

    if retry_count <= 3
      reconnect_with_rescue
      sleep 1
      retry
    else
      raise TcpConnectionError, "#{@app.name} tried #{retry_count - 1} times to reconnect but failed (#{e.class.name}, #{e.message})."
    end
  end
end

Protected Instance Methods

certificate_expired?() click to toggle source
# File lib/rpush/daemon/tcp_connection.rb, line 181
def certificate_expired?
  @ssl_context.cert.not_after && @ssl_context.cert.not_after.utc < Time.now.utc
end
certificate_expires_soon?() click to toggle source
# File lib/rpush/daemon/tcp_connection.rb, line 185
def certificate_expires_soon?
  @ssl_context.cert.not_after && @ssl_context.cert.not_after.utc < (Time.now + 1.month).utc
end
certificate_msg(msg) click to toggle source
# File lib/rpush/daemon/tcp_connection.rb, line 176
def certificate_msg(msg)
  time = @ssl_context.cert.not_after.utc.strftime('%Y-%m-%d %H:%M:%S UTC')
  "Certificate #{msg} at #{time}."
end
check_certificate_expiration() click to toggle source
# File lib/rpush/daemon/tcp_connection.rb, line 165
def check_certificate_expiration
  cert = @ssl_context.cert
  if certificate_expired?
    log_error(certificate_msg('expired'))
    raise Rpush::CertificateExpiredError.new(@app, cert.not_after)
  elsif certificate_expires_soon?
    log_warn(certificate_msg('will expire'))
    reflect(:ssl_certificate_will_expire, @app, cert.not_after)
  end
end
connect_socket() click to toggle source
# File lib/rpush/daemon/tcp_connection.rb, line 133
def connect_socket
  touch
  check_certificate_expiration

  tcp_socket = TCPSocket.new(@host, @port)
  tcp_socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
  tcp_socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true)

  # Linux
  if [:SOL_TCP, :TCP_KEEPIDLE, :TCP_KEEPINTVL, :TCP_KEEPCNT].all? { |c| Socket.const_defined?(c) }
    tcp_socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, KEEPALIVE_IDLE)
    tcp_socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, KEEPALIVE_INTERVAL)
    tcp_socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, KEEPALIVE_MAX_FAIL_PROBES)
  end

  # OSX
  if RUBY_PLATFORM =~ /darwin/
    tcp_socket.setsockopt(Socket::IPPROTO_TCP, OSX_TCP_KEEPALIVE, KEEPALIVE_IDLE)
  end

  ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, @ssl_context)
  ssl_socket.sync = true
  ssl_socket.connect
  [tcp_socket, ssl_socket]
rescue *TCP_ERRORS => error
  if error.message =~ /certificate revoked/i
    log_error('Certificate has been revoked.')
    reflect(:ssl_certificate_revoked, @app, error)
  end
  raise TcpConnectionError, "#{error.class.name}, #{error.message}"
end
idle_period_exceeded?() click to toggle source
# File lib/rpush/daemon/tcp_connection.rb, line 112
def idle_period_exceeded?
  Time.now - last_touch > self.class.idle_period
end
reconnect_idle() click to toggle source
# File lib/rpush/daemon/tcp_connection.rb, line 107
def reconnect_idle
  log_info("Idle period exceeded, reconnecting...")
  reconnect
end
setup_ssl_context() click to toggle source
# File lib/rpush/daemon/tcp_connection.rb, line 126
def setup_ssl_context
  ssl_context = OpenSSL::SSL::SSLContext.new
  ssl_context.key = OpenSSL::PKey::RSA.new(@certificate, @password)
  ssl_context.cert = OpenSSL::X509::Certificate.new(@certificate)
  ssl_context
end
touch() click to toggle source
# File lib/rpush/daemon/tcp_connection.rb, line 122
def touch
  self.last_touch = Time.now
end
write_data(data) click to toggle source
# File lib/rpush/daemon/tcp_connection.rb, line 116
def write_data(data)
  @ssl_socket.write(data)
  @ssl_socket.flush
  touch
end