xmpp4r 0.5 → 0.5.5

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 (41) hide show
  1. data/CHANGELOG +21 -0
  2. data/README.rdoc +29 -38
  3. data/Rakefile +11 -18
  4. data/data/doc/xmpp4r/examples/advanced/recvfile.rb +5 -4
  5. data/lib/xmpp4r/bytestreams/helper/filetransfer.rb +2 -0
  6. data/lib/xmpp4r/bytestreams/helper/socks5bytestreams/base.rb +1 -0
  7. data/lib/xmpp4r/bytestreams/helper/socks5bytestreams/initiator.rb +1 -1
  8. data/lib/xmpp4r/bytestreams/helper/socks5bytestreams/target.rb +4 -1
  9. data/lib/xmpp4r/caps/helper/generator.rb +2 -2
  10. data/lib/xmpp4r/client.rb +18 -4
  11. data/lib/xmpp4r/connection.rb +8 -5
  12. data/lib/xmpp4r/delay/x/delay.rb +1 -1
  13. data/lib/xmpp4r/entity_time.rb +6 -0
  14. data/lib/xmpp4r/entity_time/iq.rb +45 -0
  15. data/lib/xmpp4r/entity_time/responder.rb +57 -0
  16. data/lib/xmpp4r/httpbinding/client.rb +178 -44
  17. data/lib/xmpp4r/message.rb +46 -0
  18. data/lib/xmpp4r/muc/helper/mucclient.rb +8 -1
  19. data/lib/xmpp4r/observable.rb +9 -0
  20. data/lib/xmpp4r/observable/contact.rb +61 -0
  21. data/lib/xmpp4r/observable/helper.rb +389 -0
  22. data/lib/xmpp4r/observable/observable_thing.rb +191 -0
  23. data/lib/xmpp4r/observable/pubsub.rb +211 -0
  24. data/lib/xmpp4r/observable/subscription.rb +57 -0
  25. data/lib/xmpp4r/observable/thread_store.rb +65 -0
  26. data/lib/xmpp4r/pubsub/children/unsubscribe.rb +16 -1
  27. data/lib/xmpp4r/pubsub/helper/servicehelper.rb +48 -14
  28. data/lib/xmpp4r/rexmladdons.rb +46 -6
  29. data/lib/xmpp4r/roster/helper/roster.rb +19 -9
  30. data/lib/xmpp4r/sasl.rb +1 -1
  31. data/lib/xmpp4r/stream.rb +2 -2
  32. data/lib/xmpp4r/xmpp4r.rb +1 -1
  33. data/test/bytestreams/tc_socks5bytestreams.rb +2 -2
  34. data/test/entity_time/tc_responder.rb +65 -0
  35. data/test/roster/tc_helper.rb +2 -3
  36. data/test/tc_message.rb +52 -1
  37. data/test/tc_stream.rb +2 -2
  38. data/test/tc_streamComponent.rb +11 -1
  39. data/test/ts_xmpp4r.rb +11 -2
  40. data/xmpp4r.gemspec +20 -9
  41. metadata +41 -35
@@ -43,7 +43,7 @@ module Jabber
43
43
  begin
44
44
  # Actually this should be Time.xmlschema,
45
45
  # but "unfortunately, the 'jabber:x:delay' namespace predates" JEP 0082
46
- Time.parse(attributes['stamp'])
46
+ Time.parse("#{attributes['stamp']}Z")
47
47
  rescue ArgumentError
48
48
  nil
49
49
  end
@@ -0,0 +1,6 @@
1
+ # =XMPP4R - XMPP Library for Ruby
2
+ # License:: Ruby's license (see the LICENSE file) or GNU GPL, at your option.
3
+ # Website::http://home.gna.org/xmpp4r/
4
+
5
+ require 'xmpp4r/entity_time/iq'
6
+ require 'xmpp4r/entity_time/responder'
@@ -0,0 +1,45 @@
1
+ # =XMPP4R - XMPP Library for Ruby
2
+ # License:: Ruby's license (see the LICENSE file) or GNU GPL, at your option.
3
+ # Website::http://home.gna.org/xmpp4r/
4
+
5
+ require 'time'
6
+
7
+ module Jabber::EntityTime
8
+ NS_TIME = 'urn:xmpp:time'
9
+
10
+ class IqTime < Jabber::IqQuery
11
+ name_xmlns 'time', NS_TIME
12
+
13
+ def initialize(time=nil)
14
+ super()
15
+ set_time(time)
16
+ end
17
+
18
+ def set_time(time=nil)
19
+ time ||= Time.now
20
+
21
+ tzo = self.add_element('tzo')
22
+ tzo.add_text(time_zone_offset(time))
23
+
24
+ utc = self.add_element('utc')
25
+ utc.add_text(utc_time(time))
26
+ end
27
+
28
+ private
29
+ def utc_time(time)
30
+ raise ArgumentError, 'invalid time object' unless time.respond_to?(:utc)
31
+ time.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
32
+ end
33
+
34
+ def time_zone_offset(time)
35
+ raise ArgumentError, 'invalid time object' unless time.respond_to?(:utc_offset)
36
+
37
+ sec_offset = time.utc_offset
38
+ h_offset = sec_offset.to_i / 3600
39
+ m_offset = sec_offset.abs % 60
40
+
41
+ "%+03d:%02d"%[h_offset, m_offset]
42
+ end
43
+
44
+ end # /IqTime
45
+ end # /Jabber::EntityTime
@@ -0,0 +1,57 @@
1
+ # =XMPP4R - XMPP Library for Ruby
2
+ # License:: Ruby's license (see the LICENSE file) or GNU GPL, at your option.
3
+ # Website::http://home.gna.org/xmpp4r/
4
+
5
+ require 'xmpp4r/entity_time/iq'
6
+
7
+ module Jabber::EntityTime
8
+ ##
9
+ # XEP 202 Entity Time implementation
10
+ #
11
+ # @see http://xmpp.org/extensions/xep-0202.html
12
+ #
13
+ # @example
14
+ # <iq type='get'
15
+ # from='romeo@montague.net/orchard'
16
+ # to='juliet@capulet.com/balcony'
17
+ # id='time_1'>
18
+ # <time xmlns='urn:xmpp:time'/>
19
+ # </iq>
20
+ #
21
+ class Responder
22
+
23
+ CALLBACK_PRIORITY = 180
24
+
25
+ ##
26
+ # +to_s+ allows the responder to be added to another responder as a feature
27
+ def to_s
28
+ NS_TIME
29
+ end
30
+
31
+ def initialize(stream)
32
+ @stream = stream
33
+
34
+ stream.add_iq_callback(CALLBACK_PRIORITY, self) do |iq|
35
+ iq_callback(iq)
36
+ end
37
+ end # /initialize
38
+
39
+ def iq_callback(iq)
40
+ if iq.type == :get &&
41
+ iq.first_element('time') &&
42
+ iq.first_element('time').namespace === NS_TIME
43
+
44
+ answer = iq.answer(false)
45
+ answer.type = :result
46
+ answer.add(IqTime.new(Time.now))
47
+
48
+ @stream.send(answer)
49
+
50
+ return true
51
+ end
52
+
53
+ false
54
+ end
55
+
56
+ end # /Responder
57
+ end # /Jabber::EntityTime
@@ -7,6 +7,8 @@
7
7
  require 'xmpp4r/client'
8
8
  require 'xmpp4r/semaphore'
9
9
  require 'net/http'
10
+ require 'uri'
11
+ require 'cgi' # for escaping
10
12
 
11
13
  module Jabber
12
14
  module HTTPBinding
@@ -40,6 +42,8 @@ module Jabber
40
42
  # The server may hold this amount of stanzas
41
43
  # to reduce number of HTTP requests
42
44
  attr_accessor :http_hold
45
+ # Hook to initialize SSL parameters on Net::HTTP
46
+ attr_accessor :http_ssl_setup
43
47
 
44
48
  ##
45
49
  # Initialize
@@ -56,6 +60,50 @@ module Jabber
56
60
  @http_wait = 20
57
61
  @http_hold = 1
58
62
  @http_content_type = 'text/xml; charset=utf-8'
63
+
64
+ @no_proxy = []
65
+ @proxy_args = []
66
+ end
67
+
68
+ ##
69
+ # Set up proxy from the given url
70
+
71
+ # url:: [URI::Generic or String] of the form:
72
+ # when without proxy authentication
73
+ # http://proxy_host:proxy_port/
74
+ # when with proxy authentication
75
+ # http://proxy_user:proxy_password@proxy_host:proxy_port/
76
+ def http_proxy_uri=(uri)
77
+ uri = URI.parse(uri) unless uri.respond_to?(:host)
78
+ @proxy_args = [
79
+ uri.host,
80
+ uri.port,
81
+ uri.user,
82
+ uri.password,
83
+ ]
84
+ end
85
+
86
+ ##
87
+ # Set up proxy from the environment variables
88
+ #
89
+ # Following environment variables are considered
90
+ # HTTP_PROXY, http_proxy
91
+ # NO_PROXY, no_proxy
92
+ # HTTP_PROXY_USER, HTTP_PROXY_PASSWORD
93
+ def http_proxy_env
94
+ @proxy_args = []
95
+ env_proxy = ENV['http_proxy'] || ENV['HTTP_PROXY']
96
+ return if env_proxy.nil? or env_proxy.empty?
97
+
98
+ uri = URI.parse env_proxy
99
+ unless uri.user or uri.password then
100
+ uri.user = CGI.escape (ENV['http_proxy_user'] || ENV['HTTP_PROXY_USER']) rescue nil
101
+ uri.password = CGI.escape (ENV['http_proxy_pass'] || ENV['HTTP_PROXY_PASS']) rescue nil
102
+ end
103
+
104
+ self.http_proxy_uri = uri
105
+
106
+ @no_proxy = (ENV['NO_PROXY'] || ENV['no_proxy'] || 'localhost, 127.0.0.1').split(/\s*,\s*/)
59
107
  end
60
108
 
61
109
  ##
@@ -76,7 +124,8 @@ module Jabber
76
124
  @stream_features = {}
77
125
  @http_rid = IdGenerator.generate_id.to_i
78
126
  @pending_rid = @http_rid
79
- @pending_rid_lock = Semaphore.new
127
+ @pending_rid_lock = Mutex.new
128
+ @pending_rid_cv = [] # [ [rid, cv], [rid, cv], ... ]
80
129
 
81
130
  req_body = REXML::Element.new('body')
82
131
  req_body.attributes['rid'] = @http_rid
@@ -89,7 +138,7 @@ module Jabber
89
138
  end
90
139
  req_body.attributes['secure'] = 'true'
91
140
  req_body.attributes['xmlns'] = 'http://jabber.org/protocol/httpbind'
92
- res_body = post(req_body)
141
+ res_body = post(req_body, "sid=new rid=#{@http_rid}")
93
142
  unless res_body.name == 'body'
94
143
  raise 'Response body is no <body/> element'
95
144
  end
@@ -125,10 +174,26 @@ module Jabber
125
174
 
126
175
  ##
127
176
  # Close the session by sending
128
- # <presence type='unavailable'/>
177
+ # <body type='terminate'/>
129
178
  def close
130
179
  @status = DISCONNECTED
131
- send(Jabber::Presence.new.set_type(:unavailable))
180
+ req_body = nil
181
+ @lock.synchronize {
182
+ req_body = "<body"
183
+ req_body += " rid='#{@http_rid += 1}'"
184
+ req_body += " sid='#{@http_sid}'"
185
+ req_body += " type='terminate'"
186
+ req_body += " xmlns='http://jabber.org/protocol/httpbind'"
187
+ req_body += ">"
188
+ req_body += "<presence type='unavailable' xmlns='jabber:client'/>"
189
+ req_body += "</body>"
190
+ current_rid = @http_rid
191
+ @pending_requests += 1
192
+ @last_send = Time.now
193
+ }
194
+ res_body = post(req_body, "terminate sid=#{@http_sid} rid=#{@http_rid}")
195
+ sleep(3)
196
+ Jabber::debuglog("Connection closed")
132
197
  end
133
198
 
134
199
  private
@@ -137,32 +202,60 @@ module Jabber
137
202
  # Receive stanzas ensuring that the 'rid' order is kept
138
203
  # result:: [REXML::Element]
139
204
  def receive_elements_with_rid(rid, elements)
140
- while rid > @pending_rid
141
- @pending_rid_lock.wait
205
+ @pending_rid_lock.synchronize do
206
+ # Wait until rid == @pending_rid
207
+ if rid > @pending_rid
208
+ cv = ConditionVariable.new
209
+ @pending_rid_cv << [rid, cv]
210
+ @pending_rid_cv.sort!
211
+ while rid > @pending_rid
212
+ cv.wait(@pending_rid_lock)
213
+ end
214
+ end
142
215
  end
143
- @pending_rid = rid + 1
144
216
 
145
217
  elements.each { |e|
146
218
  receive(e)
147
219
  }
148
220
 
149
- @pending_rid_lock.run
221
+ # Signal waiting elements
222
+ @pending_rid_lock.synchronize do
223
+ @pending_rid = rid + 1 # Ensure @pending_rid is modified atomically
224
+ if @pending_rid_cv.size > 0 && @pending_rid_cv.first.first == @pending_rid
225
+ next_rid, cv = @pending_rid_cv.shift
226
+ cv.signal
227
+ end
228
+ end
150
229
  end
151
230
 
152
231
  ##
153
232
  # Do a POST request
154
- def post(body)
233
+ def post(body, debug_info)
155
234
  body = body.to_s
156
235
  request = Net::HTTP::Post.new(@uri.path)
157
236
  request.content_length = body.size
158
237
  request.body = body
159
238
  request['Content-Type'] = @http_content_type
160
- Jabber::debuglog("HTTP REQUEST (#{@pending_requests + 1}/#{@http_requests}):\n#{request.body}")
161
- response = Net::HTTP.start(@uri.host, @uri.port) { |http|
162
- http.use_ssl = true if @uri.kind_of? URI::HTTPS
239
+ Jabber::debuglog("HTTP REQUEST (#{@pending_requests}/#{@http_requests}) #{debug_info}:\n#{request.body}")
240
+
241
+ net_http_args = [@uri.host, @uri.port]
242
+ unless @proxy_args.empty?
243
+ unless no_proxy?(@uri)
244
+ net_http_args.concat @proxy_args
245
+ end
246
+ end
247
+
248
+ http = Net::HTTP.new(*net_http_args)
249
+ if @uri.kind_of? URI::HTTPS
250
+ http.use_ssl = true
251
+ @http_ssl_setup and @http_ssl_setup.call(http)
252
+ end
253
+ http.read_timeout = @http_wait * 1.1
254
+
255
+ response = http.start { |http|
163
256
  http.request(request)
164
257
  }
165
- Jabber::debuglog("HTTP RESPONSE (#{@pending_requests + 1}/#{@http_requests}): #{response.class}\n#{response.body}")
258
+ Jabber::debuglog("HTTP RESPONSE (#{@pending_requests}/#{@http_requests}) #{debug_info}: #{response.class}\n#{response.body}")
166
259
 
167
260
  unless response.kind_of? Net::HTTPSuccess
168
261
  # Unfortunately, HTTPResponses aren't exceptions
@@ -177,18 +270,30 @@ module Jabber
177
270
  body
178
271
  end
179
272
 
273
+ ##
274
+ # Check whether uri should be accessed without proxy
275
+ def no_proxy?(uri)
276
+ @no_proxy.each do |host_addr|
277
+ return true if uri.host.match(Regexp.quote(host_addr) + '$')
278
+ end
279
+ return false
280
+ end
281
+
180
282
  ##
181
283
  # Prepare data to POST and
182
284
  # handle the result
183
- def post_data(data)
285
+ def post_data(data, restart = false)
184
286
  req_body = nil
185
287
  current_rid = nil
288
+ debug_info = ''
186
289
 
187
290
  begin
188
291
  begin
189
292
  @lock.synchronize {
190
293
  # Do not send unneeded requests
191
- if data.size < 1 and @pending_requests > 0
294
+ if data.size < 1 and @pending_requests > 0 and !restart
295
+ @pending_requests += 1 # compensate for decrement in ensure clause
296
+ Jabber::debuglog "post_data: not sending excessive poll"
192
297
  return
193
298
  end
194
299
 
@@ -196,23 +301,27 @@ module Jabber
196
301
  req_body += " rid='#{@http_rid += 1}'"
197
302
  req_body += " sid='#{@http_sid}'"
198
303
  req_body += " xmlns='http://jabber.org/protocol/httpbind'"
304
+ req_body += " xml:lang='en' xmpp:restart='true' xmlns:xmpp='urn:xmpp:xbosh'" if restart
199
305
  req_body += ">"
200
- req_body += data
306
+ req_body += data unless restart
201
307
  req_body += "</body>"
202
308
  current_rid = @http_rid
309
+ debug_info = "sid=#{@http_sid} rid=#{current_rid}"
203
310
 
204
311
  @pending_requests += 1
205
312
  @last_send = Time.now
206
313
  }
207
314
 
208
- res_body = post(req_body)
315
+ res_body = post(req_body, debug_info)
209
316
 
210
317
  ensure
211
- @lock.synchronize { @pending_requests -= 1 }
318
+ @lock.synchronize {
319
+ @pending_requests -= 1
320
+ }
212
321
  end
213
322
 
214
323
  receive_elements_with_rid(current_rid, res_body.children)
215
- ensure_one_pending_request
324
+ ensure_one_pending_request if @authenticated
216
325
 
217
326
  rescue REXML::ParseException
218
327
  if @exception_block
@@ -221,53 +330,78 @@ module Jabber
221
330
  close; @exception_block.call(e, self, :parser)
222
331
  end
223
332
  else
224
- Jabber::debuglog "Exception caught when parsing HTTP response!"
333
+ Jabber::debuglog "Exception caught when parsing HTTP response! (#{debug_info})"
225
334
  close
226
335
  raise
227
336
  end
228
337
 
229
338
  rescue StandardError => e
230
- Jabber::debuglog("POST error (will retry): #{e.class}: #{e}")
339
+ Jabber::debuglog("POST error (will retry) #{debug_info}: #{e.class}: #{e}, #{e.backtrace}")
231
340
  receive_elements_with_rid(current_rid, [])
232
341
  # It's not good to resend on *any* exception,
233
342
  # but there are too many cases (Timeout, 404, 502)
234
343
  # where resending is appropriate
235
344
  # TODO: recognize these conditions and act appropriate
345
+ # FIXME: resending the same data with a new rid is wrong. should resend with the same rid
236
346
  send_data(data)
237
347
  end
238
348
  end
239
349
 
350
+ ##
351
+ # Restart stream after SASL authentication
352
+ def restart
353
+ Jabber::debuglog("Restarting after SASL")
354
+ @stream_mechanisms = []
355
+ @stream_features = {}
356
+ @features_sem = Semaphore.new
357
+ send_data('', true) # restart
358
+ end
359
+
240
360
  ##
241
361
  # Send data,
242
362
  # buffered and obeying 'polling' and 'requests' limits
243
- def send_data(data)
244
- @lock.synchronize do
245
-
246
- @send_buffer += data
247
- limited_by_polling = (@last_send + @http_polling >= Time.now)
248
- limited_by_requests = (@pending_requests + 1 > @http_requests)
249
-
250
- # Can we send?
251
- if !limited_by_polling and !limited_by_requests
252
- data = @send_buffer
253
- @send_buffer = ''
363
+ def send_data(data, restart = false)
364
+ Jabber::debuglog("send_data")
365
+
366
+ while true do # exit by return
367
+ @lock.synchronize do
368
+ if @last_send + 0.05 >= Time.now
369
+ Jabber::debuglog("send_data too fast: waiting 0.05sec")
370
+ next
371
+ end
254
372
 
255
- Thread.new do
256
- Thread.current.abort_on_exception = true
257
- post_data(data)
373
+ @send_buffer += data
374
+ limited_by_polling = false
375
+ if @pending_requests + 1 == @http_requests && @send_buffer.size == 0
376
+ limited_by_polling = (@last_send + @http_polling >= Time.now)
258
377
  end
378
+ limited_by_requests = (@pending_requests + 1 > @http_requests)
259
379
 
260
- elsif !limited_by_requests
261
- Thread.new do
262
- Thread.current.abort_on_exception = true
263
- # Defer until @http_polling has expired
264
- wait = @last_send + @http_polling - Time.now
265
- sleep(wait) if wait > 0
266
- # Ignore locking, it's already threaded ;-)
267
- send_data('')
380
+ # Can we send?
381
+ if !limited_by_polling and !limited_by_requests or !@authenticated
382
+ Jabber::debuglog("send_data non_limited")
383
+ data = @send_buffer
384
+ @send_buffer = ''
385
+
386
+ Thread.new do
387
+ Thread.current.abort_on_exception = true
388
+ post_data(data, restart)
389
+ end
390
+ return
391
+ elsif limited_by_requests
392
+ Jabber::debuglog("send_data limited by requests")
393
+ # Do nothing.
394
+ # When a pending request gets an response, it calls send_data('')
395
+ return
396
+ elsif # limited_by_polling && @authenticated
397
+ # Wait until we get some data to send, or @http_polling expires
398
+ Jabber::debuglog("send_data limited by polling: #{@http_polling}")
399
+ else
400
+ Jabber::errorlog("send_data: can't happen: pending_requests=#{@pending_requests} http_requests=#{@http_requests} send_buffer.size=#{@send_buffer.size} limited_by_polling=#{limited_by_polling} limited_by_requests=#{limited_by_requests}")
401
+ return
268
402
  end
269
403
  end
270
-
404
+ sleep(0.1)
271
405
  end
272
406
  end
273
407
  end