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: puts "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