blather 0.3.4 → 0.4.0

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 (110) hide show
  1. data/LICENSE +1 -1
  2. data/README.rdoc +41 -12
  3. data/examples/echo.rb +1 -1
  4. data/examples/execute.rb +0 -5
  5. data/examples/pubsub/cli.rb +64 -0
  6. data/examples/pubsub/ping_pong.rb +18 -0
  7. data/examples/rosterprint.rb +14 -0
  8. data/examples/xmpp4r/echo.rb +35 -0
  9. data/lib/blather.rb +35 -12
  10. data/lib/blather/client.rb +1 -1
  11. data/lib/blather/client/client.rb +19 -13
  12. data/lib/blather/client/dsl.rb +16 -0
  13. data/lib/blather/client/dsl/pubsub.rb +133 -0
  14. data/lib/blather/core_ext/active_support.rb +1 -117
  15. data/lib/blather/core_ext/active_support/inheritable_attributes.rb +117 -0
  16. data/lib/blather/core_ext/nokogiri.rb +35 -0
  17. data/lib/blather/errors.rb +3 -20
  18. data/lib/blather/errors/sasl_error.rb +3 -1
  19. data/lib/blather/errors/stanza_error.rb +10 -17
  20. data/lib/blather/errors/stream_error.rb +11 -14
  21. data/lib/blather/jid.rb +1 -0
  22. data/lib/blather/roster.rb +9 -0
  23. data/lib/blather/roster_item.rb +6 -1
  24. data/lib/blather/stanza.rb +35 -18
  25. data/lib/blather/stanza/disco.rb +7 -1
  26. data/lib/blather/stanza/disco/disco_info.rb +45 -33
  27. data/lib/blather/stanza/disco/disco_items.rb +32 -21
  28. data/lib/blather/stanza/iq.rb +13 -8
  29. data/lib/blather/stanza/iq/query.rb +16 -8
  30. data/lib/blather/stanza/iq/roster.rb +33 -22
  31. data/lib/blather/stanza/message.rb +20 -31
  32. data/lib/blather/stanza/presence.rb +3 -5
  33. data/lib/blather/stanza/presence/status.rb +13 -21
  34. data/lib/blather/stanza/presence/subscription.rb +11 -16
  35. data/lib/blather/stanza/pubsub.rb +63 -0
  36. data/lib/blather/stanza/pubsub/affiliations.rb +50 -0
  37. data/lib/blather/stanza/pubsub/create.rb +43 -0
  38. data/lib/blather/stanza/pubsub/errors.rb +9 -0
  39. data/lib/blather/stanza/pubsub/event.rb +77 -0
  40. data/lib/blather/stanza/pubsub/items.rb +63 -0
  41. data/lib/blather/stanza/pubsub/publish.rb +58 -0
  42. data/lib/blather/stanza/pubsub/retract.rb +53 -0
  43. data/lib/blather/stanza/pubsub/subscribe.rb +42 -0
  44. data/lib/blather/stanza/pubsub/subscription.rb +66 -0
  45. data/lib/blather/stanza/pubsub/subscriptions.rb +55 -0
  46. data/lib/blather/stanza/pubsub/unsubscribe.rb +42 -0
  47. data/lib/blather/stanza/pubsub_owner.rb +41 -0
  48. data/lib/blather/stanza/pubsub_owner/delete.rb +34 -0
  49. data/lib/blather/stanza/pubsub_owner/purge.rb +34 -0
  50. data/lib/blather/stream.rb +76 -168
  51. data/lib/blather/stream/client.rb +1 -2
  52. data/lib/blather/stream/component.rb +9 -5
  53. data/lib/blather/stream/features.rb +53 -0
  54. data/lib/blather/stream/features/resource.rb +63 -0
  55. data/lib/blather/stream/{sasl.rb → features/sasl.rb} +53 -52
  56. data/lib/blather/stream/features/session.rb +44 -0
  57. data/lib/blather/stream/features/tls.rb +28 -0
  58. data/lib/blather/stream/parser.rb +70 -46
  59. data/lib/blather/xmpp_node.rb +113 -52
  60. data/spec/blather/client/client_spec.rb +44 -58
  61. data/spec/blather/client/dsl/pubsub_spec.rb +465 -0
  62. data/spec/blather/client/dsl_spec.rb +19 -6
  63. data/spec/blather/core_ext/nokogiri_spec.rb +83 -0
  64. data/spec/blather/errors/sasl_error_spec.rb +8 -8
  65. data/spec/blather/errors/stanza_error_spec.rb +25 -33
  66. data/spec/blather/errors/stream_error_spec.rb +21 -16
  67. data/spec/blather/errors_spec.rb +4 -11
  68. data/spec/blather/jid_spec.rb +31 -30
  69. data/spec/blather/roster_item_spec.rb +34 -23
  70. data/spec/blather/roster_spec.rb +27 -12
  71. data/spec/blather/stanza/discos/disco_info_spec.rb +61 -42
  72. data/spec/blather/stanza/discos/disco_items_spec.rb +47 -35
  73. data/spec/blather/stanza/iq/query_spec.rb +34 -11
  74. data/spec/blather/stanza/iq/roster_spec.rb +47 -30
  75. data/spec/blather/stanza/iq_spec.rb +19 -14
  76. data/spec/blather/stanza/message_spec.rb +30 -17
  77. data/spec/blather/stanza/presence/status_spec.rb +43 -20
  78. data/spec/blather/stanza/presence/subscription_spec.rb +41 -21
  79. data/spec/blather/stanza/presence_spec.rb +34 -21
  80. data/spec/blather/stanza/pubsub/affiliations_spec.rb +57 -0
  81. data/spec/blather/stanza/pubsub/create_spec.rb +56 -0
  82. data/spec/blather/stanza/pubsub/event_spec.rb +84 -0
  83. data/spec/blather/stanza/pubsub/items_spec.rb +79 -0
  84. data/spec/blather/stanza/pubsub/publish_spec.rb +83 -0
  85. data/spec/blather/stanza/pubsub/retract_spec.rb +75 -0
  86. data/spec/blather/stanza/pubsub/subscribe_spec.rb +61 -0
  87. data/spec/blather/stanza/pubsub/subscription_spec.rb +97 -0
  88. data/spec/blather/stanza/pubsub/subscriptions_spec.rb +59 -0
  89. data/spec/blather/stanza/pubsub/unsubscribe_spec.rb +61 -0
  90. data/spec/blather/stanza/pubsub_owner/delete_spec.rb +50 -0
  91. data/spec/blather/stanza/pubsub_owner/purge_spec.rb +50 -0
  92. data/spec/blather/stanza/pubsub_owner_spec.rb +27 -0
  93. data/spec/blather/stanza/pubsub_spec.rb +62 -0
  94. data/spec/blather/stanza_spec.rb +53 -38
  95. data/spec/blather/stream/client_spec.rb +231 -88
  96. data/spec/blather/stream/component_spec.rb +14 -5
  97. data/spec/blather/stream/parser_spec.rb +145 -0
  98. data/spec/blather/xmpp_node_spec.rb +192 -96
  99. data/spec/fixtures/pubsub.rb +311 -0
  100. data/spec/spec_helper.rb +5 -4
  101. metadata +54 -18
  102. data/Rakefile +0 -139
  103. data/ext/extconf.rb +0 -65
  104. data/ext/push_parser.c +0 -209
  105. data/lib/blather/core_ext/libxml.rb +0 -28
  106. data/lib/blather/stream/resource.rb +0 -48
  107. data/lib/blather/stream/session.rb +0 -36
  108. data/lib/blather/stream/stream_handler.rb +0 -39
  109. data/lib/blather/stream/tls.rb +0 -33
  110. data/spec/blather/core_ext/libxml_spec.rb +0 -58
@@ -0,0 +1,34 @@
1
+ module Blather
2
+ class Stanza
3
+ class PubSubOwner
4
+
5
+ class Purge < PubSubOwner
6
+
7
+ register :pubsub_purge, :purge, self.registered_ns
8
+
9
+ def self.new(type = :set, host = nil, node = nil)
10
+ new_node = super(type, host)
11
+ new_node.node = node
12
+ new_node
13
+ end
14
+
15
+ def node
16
+ purge_node[:node]
17
+ end
18
+
19
+ def node=(node)
20
+ purge_node[:node] = node
21
+ end
22
+
23
+ def purge_node
24
+ unless purge_node = pubsub.find_first('ns:purge', :ns => self.class.registered_ns)
25
+ self.pubsub << (purge_node = XMPPNode.new('purge', self.document))
26
+ purge_node.namespace = self.pubsub.namespace
27
+ end
28
+ purge_node
29
+ end
30
+ end #Retract
31
+
32
+ end #PubSub
33
+ end #Stanza
34
+ end #Blather
@@ -1,6 +1,11 @@
1
1
  module Blather
2
2
 
3
3
  class Stream < EventMachine::Connection
4
+ class NoConnection < RuntimeError; end
5
+
6
+ STREAM_NS = 'http://etherx.jabber.org/streams'
7
+ attr_accessor :jid, :password
8
+
4
9
  ##
5
10
  # Start the stream between client and server
6
11
  # [client] must be an object that will respond to #call and #jid=
@@ -10,9 +15,35 @@ module Blather
10
15
  # [port] (optional) must be the port to connect to. defaults to 5222
11
16
  def self.start(client, jid, pass, host = nil, port = 5222)
12
17
  jid = JID.new jid
13
- host ||= jid.domain
18
+ if host
19
+ connect host, port, self, client, jid, pass
20
+ else
21
+ require 'resolv'
22
+ srv = []
23
+ Resolv::DNS.open { |dns| srv = dns.getresources("_xmpp-client._tcp.#{jid.domain}", Resolv::DNS::Resource::IN::SRV) }
24
+ if srv.empty?
25
+ connect jid.domain, port, self, client, jid, pass
26
+ else
27
+ srv.sort! { |a,b| (a.priority != b.priority) ? (a.priority <=> b.priority) : (b.weight <=> a.weight) }
28
+ conn = nil
29
+ srv.each { |r| break unless (conn = connect(r.target.to_s, r.port, self, client, jid, pass)) === false }
30
+ conn
31
+ end
32
+ end
33
+ end
14
34
 
15
- EM.connect host, port, self, client, jid, pass
35
+ ##
36
+ # Attempt a connection
37
+ # Stream will raise +NoConnection+ if it receives #unbind before #post_init
38
+ # this catches that and returns false prompting for another attempt
39
+ def self.connect(host, port, conn, client, jid, pass)
40
+ EM.connect host, port, conn, client, jid, pass
41
+ rescue NoConnection
42
+ false
43
+ end
44
+
45
+ [:started, :stopped, :ready, :negotiating].each do |state|
46
+ define_method("#{state}?") { @state == state }
16
47
  end
17
48
 
18
49
  ##
@@ -21,73 +52,53 @@ module Blather
21
52
  # responds to #to_s
22
53
  def send(stanza)
23
54
  #TODO Queue if not ready
24
- LOG.debug "SENDING: (#{caller[1]}) #{stanza}"
55
+ Blather.logger.debug "SENDING: (#{caller[1]}) #{stanza}"
25
56
  send_data stanza.respond_to?(:to_xml) ? stanza.to_xml : stanza.to_s
26
57
  end
27
58
 
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
59
  ##
51
60
  # Called by EM.connect to initialize stream variables
52
61
  def initialize(client, jid, pass) # :nodoc:
53
62
  super()
54
63
 
55
64
  @error = nil
56
- @client = client
65
+ @receiver = @client = client
57
66
 
58
67
  self.jid = jid
59
- @pass = pass
60
-
61
- @to = @jid.domain
68
+ @to = self.jid.domain
69
+ @password = pass
62
70
  end
63
71
 
64
72
  ##
65
73
  # Called when EM completes the connection to the server
66
74
  # this kicks off the starttls/authorize/bind process
67
75
  def connection_completed # :nodoc:
68
- # @keepalive = EM::Timer.new(60) { send_data ' ' }
69
- @state = :stopped
70
- dispatch
76
+ # @keepalive = EM::PeriodicTimer.new(60) { send_data ' ' }
77
+ start
71
78
  end
72
79
 
73
80
  ##
74
81
  # Called by EM with data from the wire
75
82
  def receive_data(data) # :nodoc:
76
- LOG.debug "\n#{'-'*30}\n"
77
- LOG.debug "<< #{data}"
78
- @parser.receive_data data
83
+ Blather.logger.debug "\n#{'-'*30}\n"
84
+ Blather.logger.debug "<< #{data}"
85
+ @parser << data
79
86
 
80
- rescue ParseWarning => e
81
- @client.receive_data e
82
87
  rescue ParseError => e
83
88
  @error = e
84
- send "<stream:error><xml-not-well-formed xmlns='urn:ietf:params:xml:ns:xmpp-streams'/></stream:error>"
89
+ send "<stream:error><xml-not-well-formed xmlns='#{StreamError::STREAM_ERR_NS}'/></stream:error>"
85
90
  stop
86
91
  end
87
92
 
93
+ def post_init
94
+ @connected = true
95
+ end
96
+
88
97
  ##
89
98
  # Called by EM when the connection is closed
90
99
  def unbind # :nodoc:
100
+ raise NoConnection unless @connected
101
+
91
102
  # @keepalive.cancel
92
103
  @state = :stopped
93
104
  @client.receive_data @error if @error
@@ -97,54 +108,41 @@ module Blather
97
108
  ##
98
109
  # Called by the parser with parsed nodes
99
110
  def receive(node) # :nodoc:
100
- LOG.debug "RECEIVING (#{node.element_name}) #{node}"
111
+ Blather.logger.debug "RECEIVING (#{node.element_name}) #{node}"
101
112
  @node = node
102
113
 
103
- if @node.find_first('//stream:error', :stream => 'http://etherx.jabber.org/streams')
104
- handle_stream_error
105
- return
106
- end
107
-
108
- case @node.element_name
109
- when 'stream'
110
- @state = :ready if @state == :stopped
111
-
112
- when 'stream:end'
113
- stop
114
-
115
- when 'features'
116
- @features = @node.children
117
- @state = :features
118
- dispatch
119
-
120
- else
121
- dispatch
122
-
114
+ if @node.namespace && @node.namespace.prefix == 'stream'
115
+ case @node.element_name
116
+ when 'stream'
117
+ @state = :ready if @state == :stopped
118
+ return
119
+ when 'error'
120
+ handle_stream_error
121
+ return
122
+ when 'end'
123
+ stop
124
+ return
125
+ when 'features'
126
+ @state = :negotiating
127
+ @receiver = Features.new(
128
+ self,
129
+ proc { ready! },
130
+ proc { |err| @error = err; stop }
131
+ )
132
+ end
123
133
  end
134
+ @receiver.receive_data @node.to_stanza
124
135
  end
125
136
 
126
137
  ##
127
138
  # Ensure the JID gets attached to the client
128
139
  def jid=(new_jid) # :nodoc:
129
- LOG.debug "NEW JID: #{new_jid}"
140
+ Blather.logger.debug "NEW JID: #{new_jid}"
130
141
  @jid = JID.new new_jid
131
142
  @client.jid = @jid
132
143
  end
133
144
 
134
145
  protected
135
- ##
136
- # Dispatch based on current state
137
- def dispatch
138
- __send__ @state
139
- end
140
-
141
- ##
142
- # Start the stream
143
- # Each time the stream is started or re-started we need to kill off the old
144
- # parser so as not to confuse it
145
- def start
146
- end
147
-
148
146
  ##
149
147
  # Stop the stream
150
148
  def stop
@@ -154,105 +152,15 @@ module Blather
154
152
  end
155
153
  end
156
154
 
157
- ##
158
- # Called when @state == :stopped to start the stream
159
- # Counter intuitive, I know
160
- def stopped
161
- start
162
- end
163
-
164
- ##
165
- # Called when @state == :ready
166
- # Simply passes the stanza to the client
167
- def ready
168
- @client.receive_data @node.to_stanza
169
- end
170
-
171
155
  def handle_stream_error
172
- @error = StreamError.import @node
156
+ @error = StreamError.import(@node)
173
157
  stop
174
- @state = :error
175
158
  end
176
159
 
177
- ##
178
- # Called when @state == :features
179
- # Runs through the list of features starting each one in turn
180
- def features
181
- feature = @features.first
182
- LOG.debug "FEATURE: #{feature}"
183
- @state = case feature ? feature.namespaces.default.href : nil
184
- when 'urn:ietf:params:xml:ns:xmpp-tls' then :establish_tls
185
- when 'urn:ietf:params:xml:ns:xmpp-sasl' then :authenticate_sasl
186
- when 'urn:ietf:params:xml:ns:xmpp-bind' then :bind_resource
187
- when 'urn:ietf:params:xml:ns:xmpp-session' then :establish_session
188
- else :ready
189
- end
190
-
191
- # Dispatch to the individual feature methods unless
192
- # feature negotiation is complete
193
- dispatch unless ready?
194
- end
195
-
196
- ##
197
- # Start TLS
198
- def establish_tls
199
- unless @tls
200
- @tls = TLS.new self
201
- # on success destroy the TLS object and restart the stream
202
- @tls.on_success { LOG.debug "TLS: SUCCESS"; @tls = nil; start }
203
- # on failure stop the stream
204
- @tls.on_failure { |err| LOG.debug "TLS: FAILURE"; @error = err; stop }
205
-
206
- @node = @features.shift
207
- end
208
- @tls.handle @node
209
- end
210
-
211
- ##
212
- # Authenticate via SASL
213
- def authenticate_sasl
214
- unless @sasl
215
- @sasl = SASL.new(self, @jid, @pass)
216
- # on success destroy the SASL object and restart the stream
217
- @sasl.on_success { LOG.debug "SASL SUCCESS"; @sasl = nil; start }
218
- # on failure set the error and stop the stream
219
- @sasl.on_failure { |err| LOG.debug "SASL FAIL"; @error = err; stop }
220
-
221
- @node = @features.shift
222
- end
223
- @sasl.handle @node
224
- end
225
-
226
- ##
227
- # Bind to the resource provided by either the client or the server
228
- def bind_resource
229
- unless @resource
230
- @resource = Resource.new self, @jid
231
- # on success destroy the Resource object, set the jid, continue along the features dispatch process
232
- @resource.on_success { |jid| LOG.debug "RESOURCE: SUCCESS"; @resource = nil; self.jid = jid; @state = :features; dispatch }
233
- # on failure end the stream
234
- @resource.on_failure { |err| LOG.debug "RESOURCE: FAILURE"; @error = err; stop }
235
-
236
- @node = @features.shift
237
- end
238
- @resource.handle @node
239
- end
240
-
241
- ##
242
- # Establish the session between client and server
243
- def establish_session
244
- unless @session
245
- @session = Session.new self, @to
246
- # on success destroy the session object, let the client know the stream has been started
247
- # then continue the features dispatch process
248
- @session.on_success { LOG.debug "SESSION: SUCCESS"; @session = nil; @client.post_init; @state = :features; dispatch }
249
- # on failure end the stream
250
- @session.on_failure { |err| LOG.debug "SESSION: FAILURE"; @error = err; stop }
251
-
252
- @node = @features.shift
253
- end
254
- @session.handle @node
160
+ def ready!
161
+ @state = :started
162
+ @receiver = @client
163
+ @client.post_init
255
164
  end
256
165
  end
257
-
258
166
  end
@@ -6,14 +6,13 @@ class Stream
6
6
  VERSION = '1.0'
7
7
  NAMESPACE = 'jabber:client'
8
8
 
9
- protected
10
9
  def start
11
10
  @parser = Parser.new self
12
11
  start_stream = <<-STREAM
13
12
  <stream:stream
14
13
  to='#{@to}'
15
14
  xmlns='#{NAMESPACE}'
16
- xmlns:stream='http://etherx.jabber.org/streams'
15
+ xmlns:stream='#{STREAM_NS}'
17
16
  version='#{VERSION}'
18
17
  xml:lang='#{LANG}'
19
18
  >
@@ -6,24 +6,28 @@ class Stream
6
6
 
7
7
  def receive(node) # :nodoc:
8
8
  if node.element_name == 'handshake'
9
- @client.post_init
9
+ ready!
10
10
  else
11
11
  super
12
12
  end
13
13
 
14
- if node.namespaces.find_by_href('http://etherx.jabber.org/streams') && node.find_first('/stream:stream[not(stream:error)]')
15
- send("<handshake>#{Digest::SHA1.hexdigest(@node['id']+@pass)}</handshake>")
14
+ if node.document.find_first('/stream:stream[not(stream:error)]', :xmlns => NAMESPACE, :stream => STREAM_NS)
15
+ send("<handshake>#{Digest::SHA1.hexdigest(@node['id']+@password)}</handshake>")
16
16
  end
17
17
  end
18
18
 
19
- protected
19
+ def send(stanza)
20
+ stanza.from ||= self.jid if stanza.respond_to?(:from) && stanza.respond_to?(:from=)
21
+ super stanza
22
+ end
23
+
20
24
  def start
21
25
  @parser = Parser.new self
22
26
  start_stream = <<-STREAM
23
27
  <stream:stream
24
28
  to='#{@jid}'
25
29
  xmlns='#{NAMESPACE}'
26
- xmlns:stream='http://etherx.jabber.org/streams'
30
+ xmlns:stream='#{STREAM_NS}'
27
31
  >
28
32
  STREAM
29
33
  send start_stream.gsub(/\s+/, ' ')
@@ -0,0 +1,53 @@
1
+ module Blather # :nodoc:
2
+ class Stream # :nodoc:
3
+
4
+ class Features
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