xmpp4r 0.5 → 0.5.5

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