| Class | Jabber::HTTPBinding::Client |
| In: |
lib/xmpp4r/httpbinding/client.rb
|
| Parent: | Jabber::Client |
This class implements an alternative Client using HTTP Binding (JEP0124).
This class is designed to be a drop-in replacement for Jabber::Client, except for the Jabber::HTTP::Client#connect method which takes an URI as argument.
HTTP requests are buffered to not exceed the negotiated ‘polling’ and ‘requests’ parameters.
Stanzas in HTTP resonses may be delayed to arrive in the order defined by ‘rid’ parameters.
Turning Jabber::debug to true will make debug output not only spit out stanzas but HTTP request/response bodies, too.
| http_content_type | [RW] | Content-Type to be used for communication (you can set this to "text/html") |
| http_hold | [RW] | The server may hold this amount of stanzas to reduce number of HTTP requests |
| http_wait | [RW] | The server should wait this value seconds if there is no stanza to be received |
Initialize
| jid: | [JID or String] |
# File lib/xmpp4r/httpbinding/client.rb, line 45
45: def initialize(jid)
46: super
47:
48: @lock = Mutex.new
49: @pending_requests = 0
50: @last_send = Time.at(0)
51: @send_buffer = ''
52:
53: @http_wait = 20
54: @http_hold = 1
55: @http_content_type = 'text/xml; charset=utf-8'
56: end
Close the session by sending <presence type=‘unavailable’/>
# File lib/xmpp4r/httpbinding/client.rb, line 139
139: def close
140: @status = DISCONNECTED
141: send(Jabber::Presence.new.set_type(:unavailable))
142: end
Set up the stream using uri as the HTTP Binding URI
You may optionally pass host and port parameters to make use of the JEP0124 ‘route’ feature.
| uri: | [URI::Generic or String] |
| host: | [String] Optional host to route to |
| port: | [Fixnum] Port for route feature |
# File lib/xmpp4r/httpbinding/client.rb, line 67
67: def connect(uri, host=nil, port=5222)
68: uri = URI::parse(uri) unless uri.kind_of? URI::Generic
69: @uri = uri
70:
71: @allow_tls = false # Shall be done at HTTP level
72: @stream_mechanisms = []
73: @stream_features = {}
74: @http_rid = IdGenerator.generate_id.to_i
75: @pending_rid = @http_rid
76: @pending_rid_lock = Mutex.new
77:
78: req_body = REXML::Element.new('body')
79: req_body.attributes['rid'] = @http_rid
80: req_body.attributes['content'] = @http_content_type
81: req_body.attributes['hold'] = @http_hold.to_s
82: req_body.attributes['wait'] = @http_wait.to_s
83: req_body.attributes['to'] = @jid.domain
84: if host
85: req_body.attributes['route'] = 'xmpp:#{host}:#{port}'
86: end
87: req_body.attributes['secure'] = 'true'
88: req_body.attributes['xmlns'] = 'http://jabber.org/protocol/httpbind'
89: res_body = post(req_body)
90: unless res_body.name == 'body'
91: raise 'Response body is no <body/> element'
92: end
93:
94: @streamid = res_body.attributes['authid']
95: @status = CONNECTED
96: @http_sid = res_body.attributes['sid']
97: @http_wait = res_body.attributes['wait'].to_i if res_body.attributes['wait']
98: @http_hold = res_body.attributes['hold'].to_i if res_body.attributes['hold']
99: @http_inactivity = res_body.attributes['inactivity'].to_i
100: @http_polling = res_body.attributes['polling'].to_i
101: @http_polling = 5 if @http_polling == 0
102: @http_requests = res_body.attributes['requests'].to_i
103: @http_requests = 1 if @http_requests == 0
104:
105: receive_elements_with_rid(@http_rid, res_body.children)
106:
107: @features_sem.run
108: end
Ensure that there is one pending request
Will be automatically called if you‘ve sent a stanza.
# File lib/xmpp4r/httpbinding/client.rb, line 128
128: def ensure_one_pending_request
129: return if is_disconnected?
130:
131: if @lock.synchronize { @pending_requests } < 1
132: send_data('')
133: end
134: end
Send a stanza, additionally with block
This method ensures a ‘jabber:client’ namespace for the stanza
# File lib/xmpp4r/httpbinding/client.rb, line 115
115: def send(xml, &block)
116: if xml.kind_of? REXML::Element
117: xml.add_namespace('jabber:client')
118: end
119:
120: super
121: end
Do a POST request
# File lib/xmpp4r/httpbinding/client.rb, line 164
164: def post(body)
165: body = body.to_s
166: request = Net::HTTP::Post.new(@uri.path)
167: request.content_length = body.size
168: request.body = body
169: request['Content-Type'] = @http_content_type
170: Jabber::debuglog("HTTP REQUEST (#{@pending_requests}/#{@http_requests}):\n#{request.body}")
171: response = Net::HTTP.start(@uri.host, @uri.port) { |http|
172: http.use_ssl = true if @uri.kind_of? URI::HTTPS
173: http.request(request)
174: }
175: Jabber::debuglog("HTTP RESPONSE (#{@pending_requests}/#{@http_requests}):\n#{response.body}")
176:
177: unless response.kind_of? Net::HTTPSuccess
178: # Unfortunately, HTTPResponses aren't exceptions
179: # TODO: rescue'ing code should be able to distinguish
180: raise Net::HTTPBadResponse, "#{response.class}"
181: end
182:
183: body = REXML::Document.new(response.body).root
184: if body.name != 'body' and body.namespace != 'http://jabber.org/protocol/httpbind'
185: raise REXML::ParseException.new('Malformed body')
186: end
187: body
188: end
Prepare data to POST and handle the result
# File lib/xmpp4r/httpbinding/client.rb, line 193
193: def post_data(data)
194: req_body = nil
195: current_rid = nil
196:
197: begin
198: begin
199: @lock.synchronize {
200: # Do not send unneeded requests
201: if data.size < 1 and @pending_requests > 0
202: return
203: end
204:
205: req_body = "<body"
206: req_body += " rid='#{@http_rid += 1}'"
207: req_body += " sid='#{@http_sid}'"
208: req_body += " xmlns='http://jabber.org/protocol/httpbind'"
209: req_body += ">"
210: req_body += data
211: req_body += "</body>"
212: current_rid = @http_rid
213:
214: @pending_requests += 1
215: @last_send = Time.now
216: }
217:
218: res_body = post(req_body)
219:
220: ensure
221: @lock.synchronize { @pending_requests -= 1 }
222: end
223:
224: receive_elements_with_rid(current_rid, res_body.children)
225: ensure_one_pending_request
226:
227: rescue REXML::ParseException
228: if @exception_block
229: Thread.new do
230: Thread.current.abort_on_exception = true
231: close; @exception_block.call(e, self, :parser)
232: end
233: else
234: Jabber::debuglog "Exception caught when parsing HTTP response!"
235: close
236: raise
237: end
238:
239: rescue StandardError => e
240: Jabber::debuglog("POST error (will retry): #{e.class}: #{e}")
241: receive_elements_with_rid(current_rid, [])
242: # It's not good to resend on *any* exception,
243: # but there are too many cases (Timeout, 404, 502)
244: # where resending is appropriate
245: # TODO: recognize these conditions and act appropriate
246: send_data(data)
247: end
248: end
Receive stanzas ensuring that the ‘rid’ order is kept
| result: | [REXML::Element] |
# File lib/xmpp4r/httpbinding/client.rb, line 149
149: def receive_elements_with_rid(rid, elements)
150: while rid > @pending_rid
151: @pending_rid_lock.lock
152: end
153: @pending_rid = rid + 1
154:
155: elements.each { |e|
156: receive(e)
157: }
158:
159: @pending_rid_lock.unlock
160: end
Send data, buffered and obeying ‘polling’ and ‘requests’ limits
# File lib/xmpp4r/httpbinding/client.rb, line 253
253: def send_data(data)
254: @lock.synchronize do
255:
256: @send_buffer += data
257: limited_by_polling = (@last_send + @http_polling >= Time.now)
258: limited_by_requests = (@pending_requests + 1 > @http_requests)
259:
260: # Can we send?
261: if !limited_by_polling and !limited_by_requests
262: data = @send_buffer
263: @send_buffer = ''
264:
265: Thread.new do
266: Thread.current.abort_on_exception = true
267: post_data(data)
268: end
269:
270: elsif !limited_by_requests
271: Thread.new do
272: Thread.current.abort_on_exception = true
273: # Defer until @http_polling has expired
274: wait = @last_send + @http_polling - Time.now
275: sleep(wait) if wait > 0
276: # Ignore locking, it's already threaded ;-)
277: send_data('')
278: end
279: end
280:
281: end
282: end