class Blather::Stream

# A pure XMPP stream.

Blather::Stream can be used to build your own handler system if Blather's doesn't suit your needs. It will take care of the entire connection process then start sending Stanza objects back to the registered client.

The client you register with Blather::Stream needs to implement the following methods:

@example Create a new stream and handle it with our own class

class MyClient
  attr :jid

  def post_init(stream, jid = nil)
    @stream = stream
    self.jid = jid
    p "Stream Started"
  end

  # Pretty print the stream
  def receive_data(stanza)
    pp stanza
  end

  def unbind
    p "Stream Ended"
  end

  def write(what)
    @stream.write what
  end
end

client = Blather::Stream.start MyClient.new, "jid@domain/res", "pass"
client.write "[pure xml over the wire]"

Constants

STREAM_NS

@private

Attributes

authcid[R]
jid[R]
password[RW]

Public Class Methods

connect(host, port, conn, client, jid, pass, connect_timeout, authcid) click to toggle source

Attempt a connection Stream will raise NoConnection if it receives unbind before post_init this catches that and returns false prompting for another attempt @private

# File lib/blather/stream.rb, line 114
def self.connect(host, port, conn, client, jid, pass, connect_timeout, authcid)
  EM.connect host, port, conn, client, jid, pass, connect_timeout, authcid
rescue NoConnection
  false
end
new(client, jid, pass, connect_timeout = nil, authcid = nil) click to toggle source

Called by EM.connect to initialize stream variables @private

Calls superclass method
# File lib/blather/stream.rb, line 137
def initialize(client, jid, pass, connect_timeout = nil, authcid = nil)
  super()

  @error = nil
  @receiver = @client = client

  self.jid = jid
  @to = self.jid.domain
  @password = pass
  @connect_timeout = connect_timeout || 180
  @authcid = authcid || self.jid.node
end
start(client, jid, pass, host = nil, port = nil, certs_directory = nil, connect_timeout = nil, opts = {}) click to toggle source

Start the stream between client and server

@param [Object] client an object that will respond to post_init, unbind receive_data @param [Blather::JID, to_s] jid the jid to authenticate with @param [String] pass the password to authenticate with @param [String, nil] host the hostname or IP to connect to. Default is to use the domain on the JID @param [Fixnum, nil] port the port to connect on. Default is the XMPP default of 5222 @param [String, nil] certs the trusted cert store in pem format to verify communication with the server is trusted. @param [Fixnum, nil] connect_timeout the number of seconds for which to wait for a successful connection @param [Hash] opts options for modifying the connection @options opts [String] :authcid The authentication ID, which defaults to the node part of the specified JID

# File lib/blather/stream.rb, line 76
def self.start(client, jid, pass, host = nil, port = nil, certs_directory = nil, connect_timeout = nil, opts = {})
  jid = JID.new jid
  port ||= 5222
  if certs_directory
    @store = CertStore.new(certs_directory)
  end
  authcid = opts[:authcid]
  if host
    connect host, port, self, client, jid, pass, connect_timeout, authcid
  else
    require 'resolv'
    srv = []
    Resolv::DNS.open do |dns|
      srv = dns.getresources(
        "_xmpp-client._tcp.#{jid.domain}",
        Resolv::DNS::Resource::IN::SRV
      )
    end

    if srv.empty?
      connect jid.domain, port, self, client, jid, pass, connect_timeout, authcid
    else
      srv.sort! do |a,b|
        (a.priority != b.priority) ? (a.priority <=> b.priority) :
                                     (b.weight <=> a.weight)
      end

      srv.detect do |r|
        not connect(r.target.to_s, r.port, self, client, jid, pass, connect_timeout, authcid) === false
      end
    end
  end
end

Public Instance Methods

connection_completed() click to toggle source

Called when EM completes the connection to the server this kicks off the starttls/authorize/bind process @private

# File lib/blather/stream.rb, line 153
def connection_completed
  if @connect_timeout
    @connect_timer = EM::Timer.new @connect_timeout do
      raise ConnectionTimeout, "Stream timed out after #{@connect_timeout} seconds." unless started?
    end
  end
  @connected = true
  start
end
jid=(new_jid) click to toggle source

Ensure the JID gets attached to the client @private

# File lib/blather/stream.rb, line 237
def jid=(new_jid)
  Blather.log "USING JID: #{new_jid}"
  @jid = JID.new new_jid
end
post_init() click to toggle source

Called by EM after the connection has started @private

# File lib/blather/stream.rb, line 189
def post_init
  @inited = true
end
receive(node) click to toggle source

Called by the parser with parsed nodes @private

# File lib/blather/stream.rb, line 209
def receive(node)
  Blather.log "RECEIVING (#{node.element_name}) #{node}"

  if node.namespace && node.namespace.prefix == 'stream'
    case node.element_name
    when 'stream'
      @state = :ready if @state == :stopped
      return
    when 'error'
      handle_stream_error node
      return
    when 'end'
      stop
      return
    when 'features'
      @state = :negotiating
      @receiver = Features.new(
        self,
        proc { ready! },
        proc { |err| @error = err; stop }
      )
    end
  end
  @receiver.receive_data node.to_stanza
end
receive_data(data) click to toggle source

Called by EM with data from the wire @private

# File lib/blather/stream.rb, line 165
def receive_data(data)
  @parser << data
rescue ParseError => e
  @error = e
  stop "<stream:error><xml-not-well-formed xmlns='#{StreamError::STREAM_ERR_NS}'/></stream:error>"
end
send(stanza) click to toggle source

Send data over the wire

@todo Queue if not ready

@param [#to_xml, to_s] stanza the stanza to send over the wire

# File lib/blather/stream.rb, line 129
def send(stanza)
  data = stanza.respond_to?(:to_xml) ? stanza.to_xml(:save_with => Nokogiri::XML::Node::SaveOptions::AS_XML) : stanza.to_s
  Blather.log "SENDING: (#{caller[1]}) #{stanza}"
  EM.next_tick { send_data data }
end
ssl_verify_peer(pem) click to toggle source

Called by EM to verify the peer certificate. If a certificate store directory has not been configured don't worry about peer verification. At least it is encrypted We Log the certificate so that you can add it to the trusted store easily if desired @private

# File lib/blather/stream.rb, line 176
def ssl_verify_peer(pem)
  # EM is supposed to close the connection when this returns false,
  # but it only does that for inbound connections, not when we
  # make a connection to another server.
  Blather.log "Checking SSL cert: #{pem}"
  return true unless @store
  @store.trusted?(pem).tap do |trusted|
    close_connection unless trusted
  end
end
unbind() click to toggle source

Called by EM when the connection is closed @private

# File lib/blather/stream.rb, line 195
def unbind
  raise NoConnection unless @inited
  raise ConnectionFailed unless @connected

  @parser.finish

  @connect_timer.cancel if @connect_timer
  @state = :stopped
  @client.receive_data @error if @error
  @client.unbind
end

Protected Instance Methods

handle_stream_error(node) click to toggle source

@private

# File lib/blather/stream.rb, line 253
def handle_stream_error(node)
  @error = StreamError.import(node)
  stop
end
ready!() click to toggle source

@private

# File lib/blather/stream.rb, line 259
def ready!
  @state = :started
  @receiver = @client
  @client.post_init self, @jid
end
stop(error = nil) click to toggle source

Stop the stream @private

# File lib/blather/stream.rb, line 245
def stop(error = nil)
  unless @state == :stopped
    @state = :stopped
    send "#{error}</stream:stream>"
  end
end