shingara-blather 0.4.8

Sign up to get free protection for your applications and to get access to all the features.
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