blather 0.2.1 → 0.2.2

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 (84) hide show
  1. data/LICENSE +2 -0
  2. data/README.rdoc +54 -29
  3. data/Rakefile +94 -13
  4. data/VERSION.yml +4 -0
  5. data/examples/drb_client.rb +2 -4
  6. data/examples/echo.rb +13 -8
  7. data/examples/pubsub/cli.rb +64 -0
  8. data/examples/pubsub/ping_pong.rb +18 -0
  9. data/examples/pubsub/pubsub_dsl.rb +52 -0
  10. data/examples/pubsub_client.rb +39 -0
  11. data/examples/rosterprint.rb +14 -0
  12. data/examples/xmpp4r/echo.rb +35 -0
  13. data/ext/extconf.rb +65 -0
  14. data/lib/blather.rb +18 -121
  15. data/lib/blather/client.rb +13 -0
  16. data/lib/blather/client/client.rb +165 -0
  17. data/lib/blather/client/dsl.rb +99 -0
  18. data/lib/blather/client/pubsub.rb +53 -0
  19. data/lib/blather/client/pubsub/node.rb +27 -0
  20. data/lib/blather/core_ext/active_support.rb +1 -0
  21. data/lib/blather/core_ext/libxml.rb +7 -1
  22. data/lib/blather/errors.rb +39 -18
  23. data/lib/blather/errors/sasl_error.rb +87 -0
  24. data/lib/blather/errors/stanza_error.rb +262 -0
  25. data/lib/blather/errors/stream_error.rb +253 -0
  26. data/lib/blather/jid.rb +9 -16
  27. data/lib/blather/roster.rb +9 -0
  28. data/lib/blather/roster_item.rb +7 -4
  29. data/lib/blather/stanza.rb +19 -25
  30. data/lib/blather/stanza/disco.rb +9 -0
  31. data/lib/blather/stanza/disco/disco_info.rb +84 -0
  32. data/lib/blather/stanza/disco/disco_items.rb +59 -0
  33. data/lib/blather/stanza/iq.rb +16 -4
  34. data/lib/blather/stanza/iq/query.rb +6 -4
  35. data/lib/blather/stanza/iq/roster.rb +38 -38
  36. data/lib/blather/stanza/pubsub.rb +33 -0
  37. data/lib/blather/stanza/pubsub/affiliations.rb +52 -0
  38. data/lib/blather/stanza/pubsub/errors.rb +9 -0
  39. data/lib/blather/stanza/pubsub/event.rb +21 -0
  40. data/lib/blather/stanza/pubsub/items.rb +59 -0
  41. data/lib/blather/stanza/pubsub/owner.rb +9 -0
  42. data/lib/blather/stanza/pubsub/subscriptions.rb +57 -0
  43. data/lib/blather/stream.rb +125 -57
  44. data/lib/blather/stream/client.rb +26 -0
  45. data/lib/blather/stream/component.rb +34 -0
  46. data/lib/blather/stream/parser.rb +17 -27
  47. data/lib/blather/stream/resource.rb +21 -24
  48. data/lib/blather/stream/sasl.rb +60 -37
  49. data/lib/blather/stream/session.rb +12 -19
  50. data/lib/blather/stream/stream_handler.rb +39 -0
  51. data/lib/blather/stream/tls.rb +22 -18
  52. data/lib/blather/xmpp_node.rb +91 -17
  53. data/spec/blather/core_ext/libxml_spec.rb +58 -0
  54. data/spec/blather/errors/sasl_error_spec.rb +56 -0
  55. data/spec/blather/errors/stanza_error_spec.rb +148 -0
  56. data/spec/blather/errors/stream_error_spec.rb +114 -0
  57. data/spec/blather/errors_spec.rb +40 -0
  58. data/spec/blather/jid_spec.rb +0 -7
  59. data/spec/blather/roster_item_spec.rb +5 -0
  60. data/spec/blather/roster_spec.rb +6 -6
  61. data/spec/blather/stanza/discos/disco_info_spec.rb +207 -0
  62. data/spec/blather/stanza/discos/disco_items_spec.rb +136 -0
  63. data/spec/blather/stanza/iq/query_spec.rb +9 -2
  64. data/spec/blather/stanza/iq/roster_spec.rb +117 -1
  65. data/spec/blather/stanza/iq_spec.rb +29 -0
  66. data/spec/blather/stanza/presence/subscription_spec.rb +12 -1
  67. data/spec/blather/stanza/presence_spec.rb +29 -0
  68. data/spec/blather/stanza/pubsub/affiliations_spec.rb +46 -0
  69. data/spec/blather/stanza/pubsub/items_spec.rb +59 -0
  70. data/spec/blather/stanza/pubsub/subscriptions_spec.rb +63 -0
  71. data/spec/blather/stanza/pubsub_spec.rb +26 -0
  72. data/spec/blather/stanza_spec.rb +13 -1
  73. data/spec/blather/stream/client_spec.rb +787 -0
  74. data/spec/blather/stream/component_spec.rb +86 -0
  75. data/spec/blather/xmpp_node_spec.rb +75 -22
  76. data/spec/fixtures/pubsub.rb +157 -0
  77. data/spec/spec_helper.rb +6 -14
  78. metadata +86 -74
  79. data/CHANGELOG +0 -5
  80. data/Manifest +0 -47
  81. data/blather.gemspec +0 -41
  82. data/lib/blather/stanza/error.rb +0 -31
  83. data/spec/blather/stream_spec.rb +0 -462
  84. data/spec/build_safe.rb +0 -20
@@ -1,8 +1,13 @@
1
1
  module Blather
2
2
 
3
- module Stream
4
-
5
- # Connect to the server
3
+ class Stream < EventMachine::Connection
4
+ ##
5
+ # Start the stream between client and server
6
+ # [client] must be an object that will respond to #call and #jid=
7
+ # [jid] must be a valid argument for JID.new (see JID)
8
+ # [pass] must be the password
9
+ # [host] (optional) must be the hostname or IP to connect to. defaults to the domain of [jid]
10
+ # [port] (optional) must be the port to connect to. defaults to 5222
6
11
  def self.start(client, jid, pass, host = nil, port = 5222)
7
12
  jid = JID.new jid
8
13
  host ||= jid.domain
@@ -10,43 +15,85 @@ module Blather
10
15
  EM.connect host, port, self, client, jid, pass
11
16
  end
12
17
 
18
+ ##
19
+ # Send data over the wire
20
+ # The argument for this can be anything that
21
+ # responds to #to_s
22
+ def send(stanza)
23
+ #TODO Queue if not ready
24
+ LOG.debug "SENDING: (#{caller[1]}) #{stanza}"
25
+ send_data stanza.respond_to?(:to_xml) ? stanza.to_xml : stanza.to_s
26
+ end
27
+
28
+ ##
29
+ # True if the stream is in the stopped state
30
+ def stopped?
31
+ @state == :stopped
32
+ end
33
+
34
+ ##
35
+ # True when the stream is in the negotiation phase.
36
+ def negotiating?
37
+ ![:stopped, :ready].include? @state
38
+ end
39
+
40
+ ##
41
+ # True when the stream is ready
42
+ # The stream is ready immediately after receiving <stream:stream>
43
+ # and before any feature negotion. Once feature negoation starts
44
+ # the stream will not be ready until all negotations have completed
45
+ # successfully.
46
+ def ready?
47
+ @state == :ready
48
+ end
49
+
50
+ ##
51
+ # Called by EM.connect to initialize stream variables
13
52
  def initialize(client, jid, pass) # :nodoc:
14
53
  super()
15
54
 
55
+ @error = nil
16
56
  @client = client
17
57
 
18
58
  self.jid = jid
19
59
  @pass = pass
20
60
 
21
61
  @to = @jid.domain
22
- @id = nil
23
- @lang = 'en'
24
- @version = '1.0'
25
- @namespace = 'jabber:client'
26
-
27
- @parser = Parser.new self
28
62
  end
29
63
 
64
+ ##
65
+ # Called when EM completes the connection to the server
66
+ # this kicks off the starttls/authorize/bind process
30
67
  def connection_completed # :nodoc:
31
68
  # @keepalive = EM::Timer.new(60) { send_data ' ' }
32
69
  @state = :stopped
33
70
  dispatch
34
71
  end
35
72
 
73
+ ##
74
+ # Called by EM with data from the wire
36
75
  def receive_data(data) # :nodoc:
37
- @parser.parse data
76
+ LOG.debug "\n#{'-'*30}\n"
77
+ LOG.debug "<< #{data}"
78
+ @parser.receive_data data
38
79
 
39
- rescue => e
40
- @client.respond_to?(:rescue) ? @client.rescue(e) : raise(e)
80
+ rescue ParseError => e
81
+ @error = e
82
+ stop
41
83
  end
42
84
 
85
+ ##
86
+ # Called by EM when the connection is closed
43
87
  def unbind # :nodoc:
44
88
  # @keepalive.cancel
45
89
  @state = :stopped
90
+ @client.call @error if @error
91
+ @client.stopped
46
92
  end
47
93
 
94
+ ##
95
+ # Called by the parser with parsed nodes
48
96
  def receive(node) # :nodoc:
49
- LOG.debug "\n"+('-'*30)+"\n"
50
97
  LOG.debug "RECEIVING (#{node.element_name}) #{node}"
51
98
  @node = node
52
99
 
@@ -55,7 +102,7 @@ module Blather
55
102
  @state = :ready if @state == :stopped
56
103
 
57
104
  when 'stream:end'
58
- @state = :stopped
105
+ stop
59
106
 
60
107
  when 'stream:features'
61
108
  @features = @node.children
@@ -63,7 +110,9 @@ module Blather
63
110
  dispatch
64
111
 
65
112
  when 'stream:error'
66
- raise StreamError.new(@node)
113
+ @error = StreamError.import @node
114
+ stop
115
+ @state = :error
67
116
 
68
117
  else
69
118
  dispatch
@@ -72,61 +121,57 @@ module Blather
72
121
  end
73
122
 
74
123
  ##
75
- # Send data over the wire
76
- def send(stanza)
77
- #TODO Queue if not ready
78
- LOG.debug "SENDING: (#{caller[1]}) #{stanza}"
79
- send_data stanza.to_s
80
- end
81
-
82
- def stopped?
83
- @state == :stopped
84
- end
85
-
86
- def ready?
87
- @state == :ready
88
- end
89
-
124
+ # Ensure the JID gets attached to the client
90
125
  def jid=(new_jid) # :nodoc:
91
126
  LOG.debug "NEW JID: #{new_jid}"
92
- new_jid = JID.new new_jid
93
- @client.jid = new_jid
94
- @jid = new_jid
127
+ @jid = JID.new new_jid
128
+ @client.jid = @jid
95
129
  end
96
130
 
97
- private
131
+ protected
132
+ ##
133
+ # Dispatch based on current state
98
134
  def dispatch
99
135
  __send__ @state
100
136
  end
101
137
 
138
+ ##
139
+ # Start the stream
140
+ # Each time the stream is started or re-started we need to kill off the old
141
+ # parser so as not to confuse it
102
142
  def start
103
- send <<-STREAM
104
- <stream:stream
105
- to='#{@to}'
106
- xmlns='#{@namespace}'
107
- xmlns:stream='http://etherx.jabber.org/streams'
108
- version='#{@version}'
109
- xml:lang='#{@lang}'
110
- >
111
- STREAM
112
143
  end
113
144
 
145
+ ##
146
+ # Stop the stream
114
147
  def stop
115
- send '</stream:stream>'
148
+ unless @state == :stopped
149
+ @state = :stopped
150
+ send '</stream:stream>'
151
+ end
116
152
  end
117
153
 
154
+ ##
155
+ # Called when @state == :stopped to start the stream
156
+ # Counter intuitive, I know
118
157
  def stopped
119
158
  start
120
159
  end
121
160
 
161
+ ##
162
+ # Called when @state == :ready
163
+ # Simply passes the stanza to the client
122
164
  def ready
123
165
  @client.call @node.to_stanza
124
166
  end
125
167
 
168
+ ##
169
+ # Called when @state == :features
170
+ # Runs through the list of features starting each one in turn
126
171
  def features
127
172
  feature = @features.first
128
173
  LOG.debug "FEATURE: #{feature}"
129
- @state = case feature ? feature['xmlns'] : nil
174
+ @state = case feature ? feature.namespaces.default.href : nil
130
175
  when 'urn:ietf:params:xml:ns:xmpp-tls' then :establish_tls
131
176
  when 'urn:ietf:params:xml:ns:xmpp-sasl' then :authenticate_sasl
132
177
  when 'urn:ietf:params:xml:ns:xmpp-bind' then :bind_resource
@@ -134,47 +179,70 @@ module Blather
134
179
  else :ready
135
180
  end
136
181
 
182
+ # Dispatch to the individual feature methods unless
183
+ # feature negotiation is complete
137
184
  dispatch unless ready?
138
185
  end
139
186
 
187
+ ##
188
+ # Start TLS
140
189
  def establish_tls
141
190
  unless @tls
142
191
  @tls = TLS.new self
143
- @tls.success { LOG.debug "TLS: SUCCESS"; @tls = nil; start }
144
- @tls.failure { LOG.debug "TLS: FAILURE"; stop }
192
+ # on success destroy the TLS object and restart the stream
193
+ @tls.on_success { LOG.debug "TLS: SUCCESS"; @tls = nil; start }
194
+ # on failure stop the stream
195
+ @tls.on_failure { |err| LOG.debug "TLS: FAILURE"; @error = err; stop }
196
+
145
197
  @node = @features.shift
146
198
  end
147
- @tls.receive @node
199
+ @tls.handle @node
148
200
  end
149
201
 
202
+ ##
203
+ # Authenticate via SASL
150
204
  def authenticate_sasl
151
205
  unless @sasl
152
206
  @sasl = SASL.new(self, @jid, @pass)
153
- @sasl.success { LOG.debug "SASL SUCCESS"; @sasl = nil; start }
154
- @sasl.failure { LOG.debug "SASL FAIL"; stop }
207
+ # on success destroy the SASL object and restart the stream
208
+ @sasl.on_success { LOG.debug "SASL SUCCESS"; @sasl = nil; start }
209
+ # on failure set the error and stop the stream
210
+ @sasl.on_failure { |err| LOG.debug "SASL FAIL"; @error = err; stop }
211
+
155
212
  @node = @features.shift
156
213
  end
157
- @sasl.receive @node
214
+ @sasl.handle @node
158
215
  end
159
216
 
217
+ ##
218
+ # Bind to the resource provided by either the client or the server
160
219
  def bind_resource
161
220
  unless @resource
162
221
  @resource = Resource.new self, @jid
163
- @resource.success { |jid| LOG.debug "RESOURCE: SUCCESS"; @resource = nil; self.jid = jid; @state = :features; dispatch }
164
- @resource.failure { LOG.debug "RESOURCE: FAILURE"; stop }
222
+ # on success destroy the Resource object, set the jid, continue along the features dispatch process
223
+ @resource.on_success { |jid| LOG.debug "RESOURCE: SUCCESS"; @resource = nil; self.jid = jid; @state = :features; dispatch }
224
+ # on failure end the stream
225
+ @resource.on_failure { |err| LOG.debug "RESOURCE: FAILURE"; @error = err; stop }
226
+
165
227
  @node = @features.shift
166
228
  end
167
- @resource.receive @node
229
+ @resource.handle @node
168
230
  end
169
231
 
232
+ ##
233
+ # Establish the session between client and server
170
234
  def establish_session
171
235
  unless @session
172
236
  @session = Session.new self, @to
173
- @session.success { LOG.debug "SESSION: SUCCESS"; @session = nil; @client.stream_started(self); @state = :features; dispatch }
174
- @session.failure { LOG.debug "SESSION: FAILURE"; stop }
237
+ # on success destroy the session object, let the client know the stream has been started
238
+ # then continue the features dispatch process
239
+ @session.on_success { LOG.debug "SESSION: SUCCESS"; @session = nil; @client.stream_started(self); @state = :features; dispatch }
240
+ # on failure end the stream
241
+ @session.on_failure { |err| LOG.debug "SESSION: FAILURE"; @error = err; stop }
242
+
175
243
  @node = @features.shift
176
244
  end
177
- @session.receive @node
245
+ @session.handle @node
178
246
  end
179
247
  end
180
248
 
@@ -0,0 +1,26 @@
1
+ module Blather
2
+ class Stream
3
+
4
+ class Client < Stream
5
+ LANG = 'en'
6
+ VERSION = '1.0'
7
+ NAMESPACE = 'jabber:client'
8
+
9
+ protected
10
+ def start
11
+ @parser = Parser.new self
12
+ start_stream = <<-STREAM
13
+ <stream:stream
14
+ to='#{@to}'
15
+ xmlns='#{NAMESPACE}'
16
+ xmlns:stream='http://etherx.jabber.org/streams'
17
+ version='#{VERSION}'
18
+ xml:lang='#{LANG}'
19
+ >
20
+ STREAM
21
+ send start_stream.gsub(/\s+/, ' ')
22
+ end
23
+ end #Client
24
+
25
+ end #Stream
26
+ end #Blather
@@ -0,0 +1,34 @@
1
+ module Blather
2
+ class Stream
3
+
4
+ class Component < Stream
5
+ NAMESPACE = 'jabber:component:accept'
6
+
7
+ def receive(node) # :nodoc:
8
+ if node.element_name == 'handshake'
9
+ @client.stream_started(self)
10
+ else
11
+ super
12
+ end
13
+
14
+ if node.element_name == 'stream:stream'
15
+ send("<handshake>#{Digest::SHA1.hexdigest(@node['id']+@pass)}</handshake>")
16
+ end
17
+ end
18
+
19
+ protected
20
+ def start
21
+ @parser = Parser.new self
22
+ start_stream = <<-STREAM
23
+ <stream:stream
24
+ to='#{@jid}'
25
+ xmlns='#{NAMESPACE}'
26
+ xmlns:stream='http://etherx.jabber.org/streams'
27
+ >
28
+ STREAM
29
+ send start_stream.gsub(/\s+/, ' ')
30
+ end
31
+ end #Client
32
+
33
+ end #Stream
34
+ end #Blather
@@ -1,11 +1,11 @@
1
1
  module Blather # :nodoc:
2
- module Stream # :nodoc:
2
+ class Stream # :nodoc:
3
3
 
4
4
  class Parser # :nodoc:
5
5
  STREAM_REGEX = %r{(/)?stream:stream}.freeze
6
6
  ERROR_REGEX = /^<(stream:[a-z]+)/.freeze
7
7
 
8
- @@debug = false
8
+ @@debug = !false
9
9
  def self.debug; @@debug; end
10
10
  def self.debug=(debug); @@debug = debug; end
11
11
 
@@ -14,31 +14,20 @@ module Stream # :nodoc:
14
14
  def initialize(receiver)
15
15
  @receiver = receiver
16
16
  @current = nil
17
-
18
- @parser = XML::SaxParser.new
19
- @parser.io = StringIO.new
20
- @parser.callbacks = self
17
+ @parser = XML::SaxPushParser.new self
21
18
  end
22
19
 
23
- def parse(string)
24
- LOG.debug "PARSING: #{string}" if @@debug
25
- if string =~ STREAM_REGEX && $1
26
- @receiver.receive XMPPNode.new('stream:end')
27
- else
28
- string << "</stream:stream>" if string =~ STREAM_REGEX && !$1
29
- string.gsub!(ERROR_REGEX, "<\\1 xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams'")
30
-
31
- @parser.string = string
32
- @parser.parse
33
- end
20
+ def receive_data(string)
21
+ LOG.debug "PARSING: (#{string})" if @@debug
22
+ @parser.receive string
34
23
  end
35
24
 
36
- NON_ATTRS = [nil, 'stream'].freeze
37
25
  def on_start_element_ns(elem, attrs, prefix, uri, namespaces)
38
- LOG.debug "START ELEM: (#{{:elem => elem, :attrs => attrs, :prefix => prefix, :ns => namespaces}.inspect})" if @@debug
26
+ LOG.debug "START ELEM: (#{{:elem => elem, :attrs => attrs, :prefix => prefix, :uri => uri, :ns => namespaces}.inspect})" if @@debug
39
27
  elem = "#{"#{prefix}:" if prefix}#{elem}"
40
28
  e = XMPPNode.new elem
41
- attrs.each { |n,v| n = "xmlns#{":#{n}" if n}" if NON_ATTRS.include?(n); e.attributes[n] = v }
29
+ XML::Namespace.new(e, prefix, uri)
30
+ attrs.each { |k,v| e.attributes[k] = v if k }
42
31
 
43
32
  if elem == 'stream:stream'
44
33
  @receiver.receive e
@@ -56,23 +45,24 @@ module Stream # :nodoc:
56
45
  end
57
46
 
58
47
  def on_end_element_ns(elem, prefix, uri)
59
- LOG.debug "END ELEM: #{{:elem => elem, :prefix => prefix, :uri => uri, :current => @current}.inspect}" if @@debug
48
+ LOG.debug "END ELEM: #{{:elem => elem, :prefix => prefix, :uri => uri}.inspect}" if @@debug
60
49
 
61
- elem = "#{"#{prefix}:" if prefix}#{elem}"
62
- return if elem =~ STREAM_REGEX
50
+ if !@current && "#{prefix}:#{elem}" =~ STREAM_REGEX
51
+ @receiver.receive XMPPNode.new('stream:end')
63
52
 
64
- if @current.parent?
53
+ elsif @current.parent?
65
54
  @current = @current.parent
66
55
 
67
56
  else
68
57
  c, @current = @current, nil
58
+ XML::Document.new.root = c
69
59
  @receiver.receive c
70
60
 
71
61
  end
62
+ end
72
63
 
73
- def on_error(msg)
74
- raise Blather::ParseError, msg.to_s
75
- end
64
+ def on_error(msg)
65
+ raise ParseError.new(msg.to_s)
76
66
  end
77
67
  end #Parser
78
68
 
@@ -1,49 +1,46 @@
1
1
  module Blather # :nodoc:
2
- module Stream # :nodoc:
2
+ class Stream # :nodoc:
3
3
 
4
- class Resource # :nodoc:
4
+ class Resource < StreamHandler # :nodoc:
5
5
  def initialize(stream, jid)
6
- @stream = stream
6
+ super stream
7
7
  @jid = jid
8
- @callbacks = {}
9
- end
10
-
11
- def success(&callback)
12
- @callbacks[:success] = callback
13
- end
14
-
15
- def failure(&callback)
16
- @callbacks[:failure] = callback
17
- end
18
-
19
- def receive(node)
20
- @node = node
21
- __send__(@node.element_name == 'iq' ? @node['type'] : @node.element_name)
22
8
  end
23
9
 
10
+ private
11
+ ##
12
+ # Respond to the bind request
13
+ # If @jid has a resource set already request it from the server
24
14
  def bind
15
+ response = Stanza::Iq.new :set
16
+ @id = response.id
17
+
25
18
  binder = XMPPNode.new('bind')
26
- binder.xmlns = 'urn:ietf:params:xml:ns:xmpp-bind'
19
+ binder.namespace = 'urn:ietf:params:xml:ns:xmpp-bind'
27
20
 
28
21
  binder << XMPPNode.new('resource', @jid.resource) if @jid.resource
29
22
 
30
- response = Stanza::Iq.new :set
31
- @id = response.id
32
23
  response << binder
33
-
34
24
  @stream.send response
35
25
  end
36
26
 
27
+ ##
28
+ # Process the result from the server
29
+ # Sets the sends the JID (now bound to a resource)
30
+ # back to the stream
37
31
  def result
38
32
  LOG.debug "RESOURE NODE #{@node}"
33
+ # ensure this is a response to our original request
39
34
  if @id == @node['id']
40
- @jid = JID.new @node.find_first('bind').content_from(:jid)
41
- @callbacks[:success].call(@jid) if @callbacks[:success]
35
+ @jid = JID.new @node.find_first('bind/jid').content
36
+ success @jid
42
37
  end
43
38
  end
44
39
 
40
+ ##
41
+ # Server returned an error
45
42
  def error
46
- @callbacks[:failure].call if @callbacks[:failure]
43
+ failure StanzaError.import(@node)
47
44
  end
48
45
  end #Resource
49
46