shingara-blather 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. data/LICENSE +22 -0
  2. data/README.md +162 -0
  3. data/examples/echo.rb +18 -0
  4. data/examples/execute.rb +16 -0
  5. data/examples/ping_pong.rb +37 -0
  6. data/examples/print_hierarchy.rb +76 -0
  7. data/examples/rosterprint.rb +14 -0
  8. data/examples/stream_only.rb +27 -0
  9. data/examples/xmpp4r/echo.rb +35 -0
  10. data/lib/blather/client/client.rb +310 -0
  11. data/lib/blather/client/dsl/pubsub.rb +170 -0
  12. data/lib/blather/client/dsl.rb +264 -0
  13. data/lib/blather/client.rb +87 -0
  14. data/lib/blather/core_ext/nokogiri.rb +40 -0
  15. data/lib/blather/errors/sasl_error.rb +43 -0
  16. data/lib/blather/errors/stanza_error.rb +107 -0
  17. data/lib/blather/errors/stream_error.rb +82 -0
  18. data/lib/blather/errors.rb +69 -0
  19. data/lib/blather/jid.rb +142 -0
  20. data/lib/blather/roster.rb +111 -0
  21. data/lib/blather/roster_item.rb +122 -0
  22. data/lib/blather/stanza/disco/disco_info.rb +176 -0
  23. data/lib/blather/stanza/disco/disco_items.rb +132 -0
  24. data/lib/blather/stanza/disco.rb +25 -0
  25. data/lib/blather/stanza/iq/query.rb +53 -0
  26. data/lib/blather/stanza/iq/roster.rb +179 -0
  27. data/lib/blather/stanza/iq.rb +138 -0
  28. data/lib/blather/stanza/message.rb +332 -0
  29. data/lib/blather/stanza/presence/status.rb +212 -0
  30. data/lib/blather/stanza/presence/subscription.rb +101 -0
  31. data/lib/blather/stanza/presence.rb +163 -0
  32. data/lib/blather/stanza/pubsub/affiliations.rb +79 -0
  33. data/lib/blather/stanza/pubsub/create.rb +65 -0
  34. data/lib/blather/stanza/pubsub/errors.rb +18 -0
  35. data/lib/blather/stanza/pubsub/event.rb +123 -0
  36. data/lib/blather/stanza/pubsub/items.rb +103 -0
  37. data/lib/blather/stanza/pubsub/publish.rb +103 -0
  38. data/lib/blather/stanza/pubsub/retract.rb +92 -0
  39. data/lib/blather/stanza/pubsub/subscribe.rb +68 -0
  40. data/lib/blather/stanza/pubsub/subscription.rb +134 -0
  41. data/lib/blather/stanza/pubsub/subscriptions.rb +81 -0
  42. data/lib/blather/stanza/pubsub/unsubscribe.rb +68 -0
  43. data/lib/blather/stanza/pubsub.rb +129 -0
  44. data/lib/blather/stanza/pubsub_owner/delete.rb +52 -0
  45. data/lib/blather/stanza/pubsub_owner/purge.rb +52 -0
  46. data/lib/blather/stanza/pubsub_owner.rb +51 -0
  47. data/lib/blather/stanza.rb +149 -0
  48. data/lib/blather/stream/client.rb +31 -0
  49. data/lib/blather/stream/component.rb +38 -0
  50. data/lib/blather/stream/features/resource.rb +63 -0
  51. data/lib/blather/stream/features/sasl.rb +187 -0
  52. data/lib/blather/stream/features/session.rb +44 -0
  53. data/lib/blather/stream/features/tls.rb +28 -0
  54. data/lib/blather/stream/features.rb +53 -0
  55. data/lib/blather/stream/parser.rb +102 -0
  56. data/lib/blather/stream.rb +231 -0
  57. data/lib/blather/xmpp_node.rb +218 -0
  58. data/lib/blather.rb +78 -0
  59. data/spec/blather/client/client_spec.rb +559 -0
  60. data/spec/blather/client/dsl/pubsub_spec.rb +462 -0
  61. data/spec/blather/client/dsl_spec.rb +143 -0
  62. data/spec/blather/core_ext/nokogiri_spec.rb +83 -0
  63. data/spec/blather/errors/sasl_error_spec.rb +33 -0
  64. data/spec/blather/errors/stanza_error_spec.rb +129 -0
  65. data/spec/blather/errors/stream_error_spec.rb +108 -0
  66. data/spec/blather/errors_spec.rb +33 -0
  67. data/spec/blather/jid_spec.rb +87 -0
  68. data/spec/blather/roster_item_spec.rb +96 -0
  69. data/spec/blather/roster_spec.rb +103 -0
  70. data/spec/blather/stanza/discos/disco_info_spec.rb +226 -0
  71. data/spec/blather/stanza/discos/disco_items_spec.rb +148 -0
  72. data/spec/blather/stanza/iq/query_spec.rb +64 -0
  73. data/spec/blather/stanza/iq/roster_spec.rb +140 -0
  74. data/spec/blather/stanza/iq_spec.rb +45 -0
  75. data/spec/blather/stanza/message_spec.rb +132 -0
  76. data/spec/blather/stanza/presence/status_spec.rb +132 -0
  77. data/spec/blather/stanza/presence/subscription_spec.rb +105 -0
  78. data/spec/blather/stanza/presence_spec.rb +66 -0
  79. data/spec/blather/stanza/pubsub/affiliations_spec.rb +57 -0
  80. data/spec/blather/stanza/pubsub/create_spec.rb +56 -0
  81. data/spec/blather/stanza/pubsub/event_spec.rb +84 -0
  82. data/spec/blather/stanza/pubsub/items_spec.rb +79 -0
  83. data/spec/blather/stanza/pubsub/publish_spec.rb +83 -0
  84. data/spec/blather/stanza/pubsub/retract_spec.rb +75 -0
  85. data/spec/blather/stanza/pubsub/subscribe_spec.rb +61 -0
  86. data/spec/blather/stanza/pubsub/subscription_spec.rb +97 -0
  87. data/spec/blather/stanza/pubsub/subscriptions_spec.rb +59 -0
  88. data/spec/blather/stanza/pubsub/unsubscribe_spec.rb +61 -0
  89. data/spec/blather/stanza/pubsub_owner/delete_spec.rb +50 -0
  90. data/spec/blather/stanza/pubsub_owner/purge_spec.rb +50 -0
  91. data/spec/blather/stanza/pubsub_owner_spec.rb +27 -0
  92. data/spec/blather/stanza/pubsub_spec.rb +67 -0
  93. data/spec/blather/stanza_spec.rb +116 -0
  94. data/spec/blather/stream/client_spec.rb +1011 -0
  95. data/spec/blather/stream/component_spec.rb +95 -0
  96. data/spec/blather/stream/parser_spec.rb +145 -0
  97. data/spec/blather/xmpp_node_spec.rb +231 -0
  98. data/spec/fixtures/pubsub.rb +311 -0
  99. data/spec/spec_helper.rb +43 -0
  100. metadata +249 -0
@@ -0,0 +1,187 @@
1
+ module Blather # :nodoc:
2
+ class Stream # :nodoc:
3
+
4
+ class SASL < Features # :nodoc:
5
+ class UnknownMechanism < BlatherError
6
+ register :sasl_unknown_mechanism
7
+ end
8
+
9
+ MECHANISMS = %w[
10
+ digest-md5
11
+ plain
12
+ anonymous
13
+ ].freeze
14
+
15
+ SASL_NS = 'urn:ietf:params:xml:ns:xmpp-sasl'.freeze
16
+ register SASL_NS
17
+
18
+ def initialize(stream, succeed, fail)
19
+ super
20
+ @jid = @stream.jid
21
+ @pass = @stream.password
22
+ @mechanisms = []
23
+ end
24
+
25
+ def receive_data(stanza)
26
+ @node = stanza
27
+ case stanza.element_name
28
+ when 'mechanisms'
29
+ available_mechanisms = stanza.children.map { |m| m.content.downcase }
30
+ @mechanisms = MECHANISMS.select { |m| available_mechanisms.include? m }
31
+ next!
32
+ when 'failure'
33
+ next!
34
+ when 'success'
35
+ @stream.start
36
+ else
37
+ if self.respond_to?(stanza.element_name)
38
+ self.__send__(stanza.element_name)
39
+ else
40
+ fail! UnknownResponse.new(stanza)
41
+ end
42
+ end
43
+ end
44
+
45
+ protected
46
+ def next!
47
+ if @jid.node == ''
48
+ process_anonymous
49
+ else
50
+ @idx = @idx ? @idx+1 : 0
51
+ authenticate_with @mechanisms[@idx]
52
+ end
53
+ end
54
+
55
+ def process_anonymous
56
+ if @mechanisms.include?('anonymous')
57
+ authenticate_with 'anonymous'
58
+ else
59
+ fail! BlatherError.new('The server does not support ANONYMOUS login. You must provide a node in the JID')
60
+ end
61
+ end
62
+
63
+ def authenticate_with(method)
64
+ method = case method
65
+ when 'digest-md5' then DigestMD5
66
+ when 'plain' then Plain
67
+ when 'anonymous' then Anonymous
68
+ when nil then fail!(SASLError.import(@node))
69
+ else next!
70
+ end
71
+
72
+ if method.is_a?(Module)
73
+ extend method
74
+ authenticate
75
+ end
76
+ end
77
+
78
+ ##
79
+ # Base64 Encoder
80
+ def b64(str)
81
+ [str].pack('m').gsub(/\s/,'')
82
+ end
83
+
84
+ ##
85
+ # Builds a standard auth node
86
+ def auth_node(mechanism, content = nil)
87
+ node = XMPPNode.new 'auth'
88
+ node.content = content if content
89
+ node.namespace = SASL_NS
90
+ node[:mechanism] = mechanism
91
+ node
92
+ end
93
+
94
+ ##
95
+ # Digest MD5 authentication
96
+ module DigestMD5 # :nodoc:
97
+ ##
98
+ # Lets the server know we're going to try DigestMD5 authentication
99
+ def authenticate
100
+ @stream.send auth_node('DIGEST-MD5')
101
+ end
102
+
103
+ ##
104
+ # Receive the challenge command.
105
+ def challenge
106
+ decode_challenge
107
+ respond
108
+ end
109
+
110
+ private
111
+ ##
112
+ # Decodes digest strings 'foo=bar,baz="faz"'
113
+ # into {'foo' => 'bar', 'baz' => 'faz'}
114
+ def decode_challenge
115
+ text = @node.content.unpack('m').first
116
+ res = {}
117
+
118
+ text.split(',').each do |statement|
119
+ key, value = statement.split('=')
120
+ res[key] = value.delete('"') unless key.empty?
121
+ end
122
+ Blather.logger.debug "CHALLENGE DECODE: #{res.inspect}"
123
+
124
+ @nonce ||= res['nonce']
125
+ @realm ||= res['realm']
126
+ end
127
+
128
+ ##
129
+ # Builds the properly encoded challenge response
130
+ def generate_response
131
+ a1 = "#{d("#{@response[:username]}:#{@response[:realm]}:#{@pass}")}:#{@response[:nonce]}:#{@response[:cnonce]}"
132
+ a2 = "AUTHENTICATE:#{@response[:'digest-uri']}"
133
+ h("#{h(a1)}:#{@response[:nonce]}:#{@response[:nc]}:#{@response[:cnonce]}:#{@response[:qop]}:#{h(a2)}")
134
+ end
135
+
136
+ ##
137
+ # Send challenge response
138
+ def respond
139
+ node = XMPPNode.new 'response'
140
+ node.namespace = SASL_NS
141
+
142
+ unless @initial_response_sent
143
+ @initial_response_sent = true
144
+ @response = {
145
+ :nonce => @nonce,
146
+ :charset => 'utf-8',
147
+ :username => @jid.node,
148
+ :realm => @realm || @jid.domain,
149
+ :cnonce => h(Time.new.to_f.to_s),
150
+ :nc => '00000001',
151
+ :qop => 'auth',
152
+ :'digest-uri' => "xmpp/#{@jid.domain}",
153
+ }
154
+ @response[:response] = generate_response
155
+ @response.each { |k,v| @response[k] = "\"#{v}\"" unless [:nc, :qop, :response, :charset].include?(k) }
156
+
157
+ Blather.logger.debug "CHALLENGE RESPONSE: #{@response.inspect}"
158
+ Blather.logger.debug "CH RESP TXT: #{@response.map { |k,v| "#{k}=#{v}" } * ','}"
159
+
160
+ # order is to simplify testing
161
+ # Ruby 1.9 eliminates the need for this with ordered hashes
162
+ order = [:nonce, :charset, :username, :realm, :cnonce, :nc, :qop, :'digest-uri', :response]
163
+ node.content = b64(order.map { |k| v = @response[k]; "#{k}=#{v}" } * ',')
164
+ end
165
+
166
+ @stream.send node
167
+ end
168
+
169
+ def d(s); Digest::MD5.digest(s); end
170
+ def h(s); Digest::MD5.hexdigest(s); end
171
+ end #DigestMD5
172
+
173
+ module Plain # :nodoc:
174
+ def authenticate
175
+ @stream.send auth_node('PLAIN', b64("#{@jid.stripped}\x00#{@jid.node}\x00#{@pass}"))
176
+ end
177
+ end #Plain
178
+
179
+ module Anonymous # :nodoc:
180
+ def authenticate
181
+ @stream.send auth_node('ANONYMOUS')
182
+ end
183
+ end #Anonymous
184
+ end #SASL
185
+
186
+ end #Stream
187
+ end
@@ -0,0 +1,44 @@
1
+ module Blather # :nodoc:
2
+ class Stream # :nodoc:
3
+
4
+ class Session < Features # :nodoc:
5
+ SESSION_NS = 'urn:ietf:params:xml:ns:xmpp-session'.freeze
6
+ register SESSION_NS
7
+
8
+ def initialize(stream, succeed, fail)
9
+ super
10
+ @to = @stream.jid.domain
11
+ end
12
+
13
+ def receive_data(stanza)
14
+ @node = stanza
15
+ case stanza.element_name
16
+ when 'session' then session
17
+ when 'iq' then check_response
18
+ else fail!(UnknownResponse.new(stanza))
19
+ end
20
+ end
21
+
22
+ private
23
+ def check_response
24
+ if @node[:type] == 'result'
25
+ succeed!
26
+ else
27
+ fail!(StanzaError.import(@node))
28
+ end
29
+ end
30
+
31
+ ##
32
+ # Send a start session command
33
+ def session
34
+ response = Stanza::Iq.new :set
35
+ response.to = @to
36
+ response << (sess = XMPPNode.new('session', response.document))
37
+ sess.namespace = SESSION_NS
38
+
39
+ @stream.send response
40
+ end
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,28 @@
1
+ module Blather # :nodoc:
2
+ class Stream # :nodoc:
3
+
4
+ class TLS < Features # :nodoc:
5
+ class TLSFailure < BlatherError
6
+ register :tls_failure
7
+ end
8
+
9
+ TLS_NS = 'urn:ietf:params:xml:ns:xmpp-tls'.freeze
10
+ register TLS_NS
11
+
12
+ def receive_data(stanza)
13
+ case stanza.element_name
14
+ when 'starttls'
15
+ @stream.send "<starttls xmlns='#{TLS_NS}'/>"
16
+ when 'proceed'
17
+ @stream.start_tls
18
+ @stream.start
19
+ # succeed!
20
+ else
21
+ fail! TLSFailure.new
22
+ end
23
+ end
24
+
25
+ end #TLS
26
+
27
+ end #Stream
28
+ end #Blather
@@ -0,0 +1,53 @@
1
+ module Blather # :nodoc:
2
+ class Stream # :nodoc:
3
+
4
+ class Features # :nodoc:
5
+ @@features = {}
6
+ def self.register(ns)
7
+ @@features[ns] = self
8
+ end
9
+
10
+ def self.from_namespace(ns)
11
+ @@features[ns]
12
+ end
13
+
14
+ def initialize(stream, succeed, fail)
15
+ @stream = stream
16
+ @succeed = succeed
17
+ @fail = fail
18
+ end
19
+
20
+ def receive_data(stanza)
21
+ if @feature
22
+ @feature.receive_data stanza
23
+ else
24
+ @features ||= stanza
25
+ next!
26
+ end
27
+ end
28
+
29
+ def next!
30
+ @idx = @idx ? @idx+1 : 0
31
+ if stanza = @features.children[@idx]
32
+ if stanza.namespaces['xmlns'] && (klass = self.class.from_namespace(stanza.namespaces['xmlns']))
33
+ @feature = klass.new @stream, proc { next! }, @fail
34
+ @feature.receive_data stanza
35
+ else
36
+ next!
37
+ end
38
+ else
39
+ succeed!
40
+ end
41
+ end
42
+
43
+ def succeed!
44
+ @succeed.call
45
+ end
46
+
47
+ def fail!(msg)
48
+ @fail.call msg
49
+ end
50
+ end
51
+
52
+ end #Stream
53
+ end #Blather
@@ -0,0 +1,102 @@
1
+ require 'nokogiri'
2
+
3
+ module Blather # :nodoc:
4
+ class Stream # :nodoc:
5
+
6
+ class Parser < Nokogiri::XML::SAX::Document # :nodoc:
7
+ NS_TO_IGNORE = %w[jabber:client jabber:component:accept]
8
+
9
+ @@debug = false
10
+ def self.debug; @@debug; end
11
+ def self.debug=(debug); @@debug = debug; end
12
+
13
+ def initialize(receiver)
14
+ @receiver = receiver
15
+ @current = nil
16
+ @namespaces = {}
17
+ @namespace_definitions = []
18
+ @parser = Nokogiri::XML::SAX::PushParser.new self
19
+ end
20
+
21
+ def receive_data(string)
22
+ Blather.logger.debug "PARSING: (#{string})" if @@debug
23
+ @parser << string
24
+ self
25
+ end
26
+ alias_method :<<, :receive_data
27
+
28
+ def start_element_namespace(elem, attrs, prefix, uri, namespaces)
29
+ Blather.logger.debug "START ELEM: (#{{:elem => elem, :attrs => attrs, :prefix => prefix, :uri => uri, :ns => namespaces}.inspect})" if @@debug
30
+
31
+ args = [elem]
32
+ args << @current.document if @current
33
+ node = XMPPNode.new *args
34
+ node.document.root = node unless @current
35
+
36
+ attrs.each do |attr|
37
+ node[attr.localname] = attr.value
38
+ end
39
+
40
+ if !@receiver.stopped?
41
+ @current << node if @current
42
+ @current = node
43
+ end
44
+
45
+ ns_keys = namespaces.map { |pre, href| pre }
46
+ namespaces.delete_if { |pre, href| NS_TO_IGNORE.include? href }
47
+ @namespace_definitions.push []
48
+ namespaces.each do |pre, href|
49
+ next if @namespace_definitions.flatten.include?(@namespaces[[pre, href]])
50
+ ns = node.add_namespace(pre, href)
51
+ @namespaces[[pre, href]] ||= ns
52
+ end
53
+ @namespaces[[prefix, uri]] ||= node.add_namespace(prefix, uri) if prefix && !ns_keys.include?(prefix)
54
+ node.namespace = @namespaces[[prefix, uri]]
55
+
56
+ deliver(node) if elem == 'stream'
57
+
58
+ # $stderr.puts "\n\n"
59
+ # $stderr.puts [elem, attrs, prefix, uri, namespaces].inspect
60
+ # $stderr.puts @namespaces.inspect
61
+ # $stderr.puts [@namespaces[[prefix, uri]].prefix, @namespaces[[prefix, uri]].href].inspect if @namespaces[[prefix, uri]]
62
+ # $stderr.puts node.inspect
63
+ # $stderr.puts node.document.to_s.gsub(/\n\s*/,'')
64
+ end
65
+
66
+ def end_element_namespace(elem, prefix, uri)
67
+ Blather.logger.debug "END ELEM: #{{:elem => elem, :prefix => prefix, :uri => uri}.inspect}" if @@debug
68
+
69
+ if elem == 'stream'
70
+ node = XMPPNode.new('end')
71
+ node.namespace = {prefix => uri}
72
+ deliver node
73
+ elsif @current.parent != @current.document
74
+ @namespace_definitions.pop
75
+ @current = @current.parent
76
+ else
77
+ deliver @current
78
+ end
79
+ end
80
+
81
+ def characters(chars = '')
82
+ Blather.logger.debug "CHARS: #{chars}" if @@debug
83
+ @current << Nokogiri::XML::Text.new(chars, @current.document) if @current
84
+ end
85
+
86
+ def warning(msg)
87
+ Blather.logger.debug "PARSE WARNING: #{msg}" if @@debug
88
+ end
89
+
90
+ def error(msg)
91
+ raise ParseError.new(msg)
92
+ end
93
+
94
+ private
95
+ def deliver(node)
96
+ @current, @namespaces, @namespace_definitions = nil, {}, []
97
+ @receiver.receive node
98
+ end
99
+ end #Parser
100
+
101
+ end #Stream
102
+ end #Blather
@@ -0,0 +1,231 @@
1
+ module Blather
2
+
3
+ # # A pure XMPP stream.
4
+ #
5
+ # Blather::Stream can be used to build your own handler system if Blather's
6
+ # doesn't suit your needs. It will take care of the entire connection
7
+ # process then start sending Stanza objects back to the registered client.
8
+ #
9
+ # The client you register with Blather::Stream needs to implement the following
10
+ # methods:
11
+ # * #post_init(stream, jid = nil)
12
+ # Called after the stream has been initiated.
13
+ # @param [Blather::Stream] stream is the connected stream object
14
+ # @param [Blather::JID, nil] jid is the full JID as recognized by the server
15
+ #
16
+ # * #receive_data(stanza)
17
+ # Called every time the stream receives a new stanza
18
+ # @param [Blather::Stanza] stanza a stanza object from the server
19
+ #
20
+ # * #unbind
21
+ # Called when the stream is shutdown. This will be called regardless of which
22
+ # side shut the stream down.
23
+ #
24
+ # @example Create a new stream and handle it with our own class
25
+ # class MyClient
26
+ # attr :jid
27
+ #
28
+ # def post_init(stream, jid = nil)
29
+ # @stream = stream
30
+ # self.jid = jid
31
+ # p "Stream Started"
32
+ # end
33
+ #
34
+ # # Pretty print the stream
35
+ # def receive_data(stanza)
36
+ # pp stanza
37
+ # end
38
+ #
39
+ # def unbind
40
+ # p "Stream Ended"
41
+ # end
42
+ #
43
+ # def write(what)
44
+ # @stream.write what
45
+ # end
46
+ # end
47
+ #
48
+ # client = Blather::Stream.start MyClient.new, "jid@domain/res", "pass"
49
+ # client.write "[pure xml over the wire]"
50
+ class Stream < EventMachine::Connection
51
+ class NoConnection < RuntimeError; end
52
+
53
+ STREAM_NS = 'http://etherx.jabber.org/streams'
54
+ attr_accessor :password
55
+ attr_reader :jid
56
+
57
+ # Start the stream between client and server
58
+ #
59
+ # @param [Object] client an object that will respond to #post_init,
60
+ # #unbind #receive_data
61
+ # @param [Blather::JID, #to_s] jid the jid to authenticate with
62
+ # @param [String] pass the password to authenticate with
63
+ # @param [String, nil] host the hostname or IP to connect to. Default is
64
+ # to use the domain on the JID
65
+ # @param [Fixnum, nil] port the port to connect on. Default is the XMPP
66
+ # default of 5222
67
+ def self.start(client, jid, pass, host = nil, port = 5222)
68
+ jid = JID.new jid
69
+ if host
70
+ connect host, port, self, client, jid, pass
71
+ else
72
+ require 'resolv'
73
+ srv = []
74
+ Resolv::DNS.open do |dns|
75
+ srv = dns.getresources(
76
+ "_xmpp-client._tcp.#{jid.domain}",
77
+ Resolv::DNS::Resource::IN::SRV
78
+ )
79
+ end
80
+
81
+ if srv.empty?
82
+ connect jid.domain, port, self, client, jid, pass
83
+ else
84
+ srv.sort! do |a,b|
85
+ (a.priority != b.priority) ? (a.priority <=> b.priority) :
86
+ (b.weight <=> a.weight)
87
+ end
88
+
89
+ srv.detect do |r|
90
+ not connect(r.target.to_s, r.port, self, client, jid, pass) === false
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ # Attempt a connection
97
+ # Stream will raise +NoConnection+ if it receives #unbind before #post_init
98
+ # this catches that and returns false prompting for another attempt
99
+ # @private
100
+ def self.connect(host, port, conn, client, jid, pass)
101
+ EM.connect host, port, conn, client, jid, pass
102
+ rescue NoConnection
103
+ false
104
+ end
105
+
106
+ [:started, :stopped, :ready, :negotiating].each do |state|
107
+ define_method("#{state}?") { @state == state }
108
+ end
109
+
110
+ # Send data over the wire
111
+ #
112
+ # @todo Queue if not ready
113
+ #
114
+ # @param [#to_xml, #to_s] stanza the stanza to send over the wire
115
+ def send(stanza)
116
+ Blather.logger.debug "SENDING: (#{caller[1]}) #{stanza}"
117
+ send_data stanza.respond_to?(:to_xml) ? stanza.to_xml : stanza.to_s
118
+ end
119
+
120
+ # Called by EM.connect to initialize stream variables
121
+ # @private
122
+ def initialize(client, jid, pass)
123
+ super()
124
+
125
+ @error = nil
126
+ @receiver = @client = client
127
+
128
+ self.jid = jid
129
+ @to = self.jid.domain
130
+ @password = pass
131
+ end
132
+
133
+ # Called when EM completes the connection to the server
134
+ # this kicks off the starttls/authorize/bind process
135
+ # @private
136
+ def connection_completed
137
+ # @keepalive = EM::PeriodicTimer.new(60) { send_data ' ' }
138
+ start
139
+ end
140
+
141
+ # Called by EM with data from the wire
142
+ # @private
143
+ def receive_data(data)
144
+ Blather.logger.debug "\n#{'-'*30}\n"
145
+ Blather.logger.debug "STREAM IN: #{data}"
146
+ @parser << data
147
+
148
+ rescue ParseError => e
149
+ @error = e
150
+ send "<stream:error><xml-not-well-formed xmlns='#{StreamError::STREAM_ERR_NS}'/></stream:error>"
151
+ stop
152
+ end
153
+
154
+ # Called by EM after the connection has started
155
+ # @private
156
+ def post_init
157
+ @connected = true
158
+ end
159
+
160
+ # Called by EM when the connection is closed
161
+ # @private
162
+ def unbind
163
+ raise NoConnection unless @connected
164
+
165
+ # @keepalive.cancel
166
+ @state = :stopped
167
+ @client.receive_data @error if @error
168
+ @client.unbind
169
+ end
170
+
171
+ # Called by the parser with parsed nodes
172
+ # @private
173
+ def receive(node)
174
+ Blather.logger.debug "RECEIVING (#{node.element_name}) #{node}"
175
+ @node = node
176
+
177
+ if @node.namespace && @node.namespace.prefix == 'stream'
178
+ case @node.element_name
179
+ when 'stream'
180
+ @state = :ready if @state == :stopped
181
+ return
182
+ when 'error'
183
+ handle_stream_error
184
+ return
185
+ when 'end'
186
+ stop
187
+ return
188
+ when 'features'
189
+ @state = :negotiating
190
+ @receiver = Features.new(
191
+ self,
192
+ proc { ready! },
193
+ proc { |err| @error = err; stop }
194
+ )
195
+ end
196
+ end
197
+ @receiver.receive_data @node.to_stanza
198
+ end
199
+
200
+ # Ensure the JID gets attached to the client
201
+ # @private
202
+ def jid=(new_jid)
203
+ Blather.logger.debug "NEW JID: #{new_jid}"
204
+ @jid = JID.new new_jid
205
+ end
206
+
207
+ protected
208
+ # Stop the stream
209
+ # @private
210
+ def stop
211
+ unless @state == :stopped
212
+ @state = :stopped
213
+ send '</stream:stream>'
214
+ end
215
+ end
216
+
217
+ # @private
218
+ def handle_stream_error
219
+ @error = StreamError.import(@node)
220
+ stop
221
+ end
222
+
223
+ # @private
224
+ def ready!
225
+ @state = :started
226
+ @receiver = @client
227
+ @client.post_init self, @jid
228
+ end
229
+ end # Stream
230
+
231
+ end # Blather