gloox  1.1-svn
connectionbosh.cpp
1 /*
2  * Copyright (c) 2007-2009 by Jakob Schroeter <js@camaya.net>
3  * This file is part of the gloox library. http://camaya.net/gloox
4  *
5  * This software is distributed under a license. The full license
6  * agreement can be found in the file LICENSE in this distribution.
7  * This software may not be copied, modified, sold or distributed
8  * other than expressed in the named license agreement.
9  *
10  * This software is distributed without any warranty.
11  */
12 
13 #include "config.h"
14 
15 #include "gloox.h"
16 
17 #include "connectionbosh.h"
18 #include "logsink.h"
19 #include "prep.h"
20 #include "tag.h"
21 #include "util.h"
22 
23 #include <string>
24 #include <cstdlib>
25 #include <cctype>
26 #include <algorithm>
27 
28 namespace gloox
29 {
30 
31  ConnectionBOSH::ConnectionBOSH( ConnectionBase* connection, const LogSink& logInstance,
32  const std::string& boshHost, const std::string& xmppServer,
33  int xmppPort )
34  : ConnectionBase( 0 ),
35  m_logInstance( logInstance ), m_parser( this ), m_boshHost( boshHost ), m_path( "/http-bind/" ),
36  m_rid( 0 ), m_initialStreamSent( false ), m_openRequests( 0 ),
37  m_maxOpenRequests( 2 ), m_wait( 30 ), m_hold( 2 ), m_streamRestart( false ),
38  m_lastRequestTime( std::time( 0 ) ), m_minTimePerRequest( 0 ), m_bufferContentLength( 0 ),
39  m_connMode( ModePipelining )
40  {
41  initInstance( connection, xmppServer, xmppPort );
42  }
43 
45  const LogSink& logInstance, const std::string& boshHost,
46  const std::string& xmppServer, int xmppPort )
47  : ConnectionBase( cdh ),
48  m_logInstance( logInstance ), m_parser( this ), m_boshHost( boshHost ), m_path( "/http-bind/" ),
49  m_rid( 0 ), m_initialStreamSent( false ), m_openRequests( 0 ),
50  m_maxOpenRequests( 2 ), m_wait( 30 ), m_hold( 2 ), m_streamRestart( false ),
51  m_lastRequestTime( std::time( 0 ) ), m_minTimePerRequest( 0 ), m_bufferContentLength( 0 ),
52  m_connMode( ModePipelining )
53  {
54  initInstance( connection, xmppServer, xmppPort );
55  }
56 
57  void ConnectionBOSH::initInstance( ConnectionBase* connection, const std::string& xmppServer,
58  const int xmppPort )
59  {
60 // FIXME: check return value
61  prep::idna( xmppServer, m_server );
62  m_port = xmppPort;
63  if( m_port != -1 )
64  {
65  m_boshedHost = m_boshHost + ":" + util::int2string( m_port );
66  }
67 
68  // drop this connection into our pool of available connections
69  if( connection )
70  {
71  connection->registerConnectionDataHandler( this );
72  m_connectionPool.push_back( connection );
73  }
74  }
75 
77  {
78  util::clearList( m_activeConnections );
79  util::clearList( m_connectionPool );
80  }
81 
82  ConnectionBase* ConnectionBOSH::newInstance() const
83  {
84  ConnectionBase* pBaseConn = 0;
85 
86  if( !m_connectionPool.empty() )
87  {
88  pBaseConn = m_connectionPool.front()->newInstance();
89  }
90  else if( !m_activeConnections.empty() )
91  {
92  pBaseConn = m_activeConnections.front()->newInstance();
93  }
94  else
95  {
96  return 0;
97  }
98 
99  return new ConnectionBOSH( m_handler, pBaseConn, m_logInstance,
100  m_boshHost, m_server, m_port );
101  }
102 
103  ConnectionError ConnectionBOSH::connect()
104  {
105  if( m_state >= StateConnecting )
106  return ConnNoError;
107 
108  if( !m_handler )
109  return ConnNotConnected;
110 
112  m_logInstance.dbg( LogAreaClassConnectionBOSH,
113  "bosh initiating connection to server: " +
114  ( ( m_connMode == ModePipelining ) ? std::string( "Pipelining" )
115  : ( ( m_connMode == ModeLegacyHTTP ) ? std::string( "LegacyHTTP" )
116  : std::string( "PersistentHTTP" ) ) ) );
117  getConnection();
118  return ConnNoError; // FIXME?
119  }
120 
121  void ConnectionBOSH::disconnect()
122  {
123  if( ( m_connMode == ModePipelining && m_activeConnections.empty() )
124  || ( m_connectionPool.empty() && m_activeConnections.empty() ) )
125  return;
126 
127  if( m_state != StateDisconnected )
128  {
129  ++m_rid;
130 
131  std::string requestBody = "<body rid='" + util::int2string( m_rid ) + "' ";
132  requestBody += "sid='" + m_sid + "' ";
133  requestBody += "type='terminal' ";
134  requestBody += "xml:lang='en' ";
135  requestBody += "xmlns='" + XMLNS_HTTPBIND + "'";
136  if( m_sendBuffer.empty() ) // Make sure that any data in the send buffer gets sent
137  requestBody += "/>";
138  else
139  {
140  requestBody += ">" + m_sendBuffer + "</body>";
141  m_sendBuffer = EmptyString;
142  }
143  sendRequest( requestBody );
144 
145  m_logInstance.dbg( LogAreaClassConnectionBOSH, "bosh disconnection request sent" );
146  }
147  else
148  {
149  m_logInstance.err( LogAreaClassConnectionBOSH,
150  "disconnecting from server in a non-graceful fashion" );
151  }
152 
153  util::ForEach( m_activeConnections, &ConnectionBase::disconnect );
154  util::ForEach( m_connectionPool, &ConnectionBase::disconnect );
155 
157  if( m_handler )
159  }
160 
161  ConnectionError ConnectionBOSH::recv( int timeout )
162  {
163  if( m_state == StateDisconnected )
164  return ConnNotConnected;
165 
166  if( !m_connectionPool.empty() )
167  m_connectionPool.front()->recv( 0 );
168  if( !m_activeConnections.empty() )
169  m_activeConnections.front()->recv( timeout );
170 
171  // If there are no open requests then the spec allows us to send an empty request...
172  // (Some CMs do not obey this, it seems)
173  if( ( m_openRequests == 0 || m_sendBuffer.size() > 0 ) && m_state == StateConnected )
174  {
175  m_logInstance.dbg( LogAreaClassConnectionBOSH,
176  "Sending empty request (or there is data in the send buffer)" );
177  sendXML();
178  }
179 
180  return ConnNoError; // FIXME?
181  }
182 
183  bool ConnectionBOSH::send( const std::string& data )
184  {
185 
186  if( m_state == StateDisconnected )
187  return false;
188 
189  if( data.substr( 0, 2 ) == "<?" )
190  {
191 // if( m_initialStreamSent )
192  {
193  m_streamRestart = true;
194  sendXML();
195  return true;
196  }
197 // else
198 // {
199 // m_initialStreamSent = true;
200 // m_logInstance.dbg( LogAreaClassConnectionBOSH, "initial <stream:stream> dropped" );
201 // return true;
202 // }
203  }
204  else if( data == "</stream:stream>" )
205  return true;
206 
207  m_sendBuffer += data;
208  sendXML();
209 
210  return true;
211  }
212 
213  /* Sends XML. Wraps data in a <body/> tag, and then passes to sendRequest(). */
214  bool ConnectionBOSH::sendXML()
215  {
216  if( m_state != StateConnected )
217  {
218  m_logInstance.warn( LogAreaClassConnectionBOSH,
219  "Data sent before connection established (will be buffered)" );
220  return false;
221  }
222 
223  if( m_sendBuffer.empty() )
224  {
225  time_t now = time( 0 );
226  unsigned int delta = (int)(now - m_lastRequestTime);
227  if( delta < m_minTimePerRequest && m_openRequests > 0 )
228  {
229  m_logInstance.dbg( LogAreaClassConnectionBOSH, "Too little time between requests: " + util::int2string( delta ) + " seconds" );
230  return false;
231  }
232  m_logInstance.dbg( LogAreaClassConnectionBOSH, "Send buffer is empty, sending empty request" );
233  }
234 
235  ++m_rid;
236 
237  std::string requestBody = "<body rid='" + util::int2string( m_rid ) + "' ";
238  requestBody += "sid='" + m_sid + "' ";
239  requestBody += "xmlns='" + XMLNS_HTTPBIND + "'";
240 
241  if( m_streamRestart )
242  {
243  requestBody += " xmpp:restart='true' to='" + m_server + "' xml:lang='en' xmlns:xmpp='"
244  + XMLNS_XMPP_BOSH + "' />";
245  m_logInstance.dbg( LogAreaClassConnectionBOSH, "Restarting stream" );
246  }
247  else
248  {
249  requestBody += ">" + m_sendBuffer + "</body>";
250  }
251  // Send a request. Force if we are not sending an empty request, or if there are no connections open
252  if( sendRequest( requestBody ) )
253  {
254  m_logInstance.dbg( LogAreaClassConnectionBOSH, "Successfully sent m_sendBuffer" );
255  m_sendBuffer = EmptyString;
256  m_streamRestart = false;
257  }
258  else
259  {
260  --m_rid; // I think... (may need to rethink when acks are implemented)
261  m_logInstance.warn( LogAreaClassConnectionBOSH,
262  "Unable to send. Connection not complete, or too many open requests,"
263  " so added to buffer.\n" );
264  }
265 
266  return true;
267  }
268 
269  /* Chooses the appropriate connection, or opens a new one if necessary. Wraps xml in HTTP and sends. */
270  bool ConnectionBOSH::sendRequest( const std::string& xml )
271  {
272  ConnectionBase* conn = getConnection();
273  if( !conn )
274  return false;
275 
276  std::string request = "POST " + m_path;
277 
278  if( m_connMode == ModeLegacyHTTP )
279  {
280  request += " HTTP/1.0\r\n";
281  request += "Connection: close\r\n";
282  }
283  else
284  {
285  request += " HTTP/1.1\r\n";
286  request += "Connection: keep-alive\r\n";
287  }
288 
289  request += "Host: " + m_boshedHost + "\r\n";
290  request += "Content-Type: text/xml; charset=utf-8\r\n";
291  request += "Content-Length: " + util::int2string( xml.length() ) + "\r\n";
292  request += "User-Agent: gloox/" + GLOOX_VERSION + "\r\n\r\n";
293  request += xml;
294 
295 
296  if( conn->send( request ) )
297  {
298  m_lastRequestTime = time( 0 );
299  ++m_openRequests;
300  return true;
301  }
302 // else // FIXME What to do in this case?
303 // printf( "Error while trying to send on socket (state: %d)\n", conn->state() );
304 
305  return false;
306  }
307 
308  bool ci_equal( char ch1, char ch2 )
309  {
310  return std::toupper( (unsigned char)ch1 ) == std::toupper( (unsigned char)ch2 );
311  }
312 
313  std::string::size_type ci_find( const std::string& str1, const std::string& str2 )
314  {
315  std::string::const_iterator pos = std::search( str1.begin(), str1.end(),
316  str2.begin(), str2.end(), ci_equal );
317  if( pos == str1.end() )
318  return std::string::npos;
319  else
320  return std::distance( str1.begin(), pos );
321  }
322 
323  const std::string ConnectionBOSH::getHTTPField( const std::string& field )
324  {
325  std::string::size_type fp = ci_find( m_bufferHeader, "\r\n" + field + ": " );
326 
327  if( fp == std::string::npos )
328  return EmptyString;
329 
330  fp += field.length() + 4;
331 
332  const std::string::size_type fp2 = m_bufferHeader.find( "\r\n", fp );
333  if( fp2 == std::string::npos )
334  return EmptyString;
335 
336  return m_bufferHeader.substr( fp, fp2 - fp );
337  }
338 
339  ConnectionError ConnectionBOSH::receive()
340  {
342  while( m_state != StateDisconnected && ( err = recv( 10 ) ) == ConnNoError )
343  ;
344  return err == ConnNoError ? ConnNotConnected : err;
345  }
346 
347  void ConnectionBOSH::cleanup()
348  {
350 
351  util::ForEach( m_activeConnections, &ConnectionBase::cleanup );
352  util::ForEach( m_connectionPool, &ConnectionBase::cleanup );
353  }
354 
355  void ConnectionBOSH::getStatistics( long int& totalIn, long int& totalOut )
356  {
357  util::ForEach( m_activeConnections, &ConnectionBase::getStatistics, totalIn, totalOut );
358  util::ForEach( m_connectionPool, &ConnectionBase::getStatistics, totalIn, totalOut );
359  }
360 
361  void ConnectionBOSH::handleReceivedData( const ConnectionBase* /*connection*/,
362  const std::string& data )
363  {
364  m_buffer += data;
365 // printf( "!!!!!!buffer\n\n%s\n\n", m_buffer.c_str() );
366  std::string::size_type headerLength = 0;
367  while( ( headerLength = m_buffer.find( "\r\n\r\n" ) ) != std::string::npos )
368  {
369  m_bufferHeader = m_buffer.substr( 0, headerLength + 2 );
370 
371  const std::string& statusCode = m_bufferHeader.substr( 9, 3 );
372  if( statusCode != "200" )
373  {
374  m_logInstance.warn( LogAreaClassConnectionBOSH,
375  "Received error via legacy HTTP status code: " + statusCode
376  + ". Disconnecting." );
377  m_state = StateDisconnected; // As per XEP, consider connection broken
378  disconnect();
379  }
380 
381  m_bufferContentLength = strtol( getHTTPField( "Content-Length" ).c_str(), 0, 10 );
382  if( !m_bufferContentLength )
383  return;
384 
385  if( m_connMode != ModeLegacyHTTP && ( getHTTPField( "Connection" ) == "close"
386  || m_bufferHeader.substr( 0, 8 ) == "HTTP/1.0" ) )
387  {
388  m_logInstance.dbg( LogAreaClassConnectionBOSH,
389  "Server indicated lack of support for HTTP/1.1 - falling back to HTTP/1.0" );
390  m_connMode = ModeLegacyHTTP;
391  }
392 
393  if( m_buffer.length() >= ( headerLength + 4 + m_bufferContentLength ) )
394  {
395  putConnection();
396  --m_openRequests;
397  std::string xml = m_buffer.substr( headerLength + 4, m_bufferContentLength );
398  m_parser.feed( xml );
399  m_buffer.erase( 0, headerLength + 4 + m_bufferContentLength );
400  m_bufferContentLength = 0;
401  m_bufferHeader = EmptyString;
402  }
403  else
404  {
405  m_logInstance.warn( LogAreaClassConnectionBOSH, "buffer length mismatch" );
406  break;
407  }
408  }
409  }
410 
411  void ConnectionBOSH::handleConnect( const ConnectionBase* /*connection*/ )
412  {
413  if( m_state == StateConnecting )
414  {
415  m_rid = rand() % 100000 + 1728679472;
416 
417  Tag requestBody( "body" );
418  requestBody.setXmlns( XMLNS_HTTPBIND );
419  requestBody.setXmlns( XMLNS_XMPP_BOSH, "xmpp" );
420 
421  requestBody.addAttribute( "content", "text/xml; charset=utf-8" );
422  requestBody.addAttribute( "hold", (long)m_hold );
423  requestBody.addAttribute( "rid", (long)m_rid );
424  requestBody.addAttribute( "ver", "1.6" );
425  requestBody.addAttribute( "wait", (long)m_wait );
426  requestBody.addAttribute( "ack", 0 );
427  requestBody.addAttribute( "secure", "false" );
428  requestBody.addAttribute( "route", "xmpp:" + m_server + ":5222" );
429  requestBody.addAttribute( "xml:lang", "en" );
430  requestBody.addAttribute( "xmpp:version", "1.0" );
431  requestBody.addAttribute( "to", m_server );
432 
433  m_logInstance.dbg( LogAreaClassConnectionBOSH, "sending bosh connection request" );
434  sendRequest( requestBody.xml() );
435  }
436  }
437 
438  void ConnectionBOSH::handleDisconnect( const ConnectionBase* /*connection*/,
439  ConnectionError reason )
440  {
441  if( m_handler && m_state == StateConnecting )
442  {
444  m_handler->handleDisconnect( this, reason );
445  return;
446  }
447 
448  switch( m_connMode ) // FIXME avoid that if we're disconnecting on purpose
449  {
450  case ModePipelining:
451  m_connMode = ModeLegacyHTTP; // Server seems not to support pipelining
452  m_logInstance.dbg( LogAreaClassConnectionBOSH,
453  "connection closed - falling back to HTTP/1.0 connection method" );
454  break;
455  case ModeLegacyHTTP:
456  case ModePersistentHTTP:
457  // FIXME do we need to do anything here?
458 // printf( "A TCP connection %p was disconnected (reason: %d).\n", connection, reason );
459  break;
460  }
461  }
462 
463  void ConnectionBOSH::handleTag( Tag* tag )
464  {
465  if( !m_handler || tag->name() != "body" )
466  return;
467 
468  if( m_streamRestart )
469  {
470  m_streamRestart = false;
471  m_logInstance.dbg( LogAreaClassConnectionBOSH, "sending spoofed <stream:stream>" );
472  m_handler->handleReceivedData( this, "<?xml version='1.0' ?>"
473  "<stream:stream xmlns:stream='http://etherx.jabber.org/streams'"
474  " xmlns='" + XMLNS_CLIENT + "' version='" + XMPP_STREAM_VERSION_MAJOR
475  + "." + XMPP_STREAM_VERSION_MINOR + "' from='" + m_server + "' id ='"
476  + m_sid + "' xml:lang='en'>" );
477  }
478 
479  if( tag->hasAttribute( "sid" ) )
480  {
482  m_sid = tag->findAttribute( "sid" );
483 
484  if( tag->hasAttribute( "requests" ) )
485  {
486  const int serverRequests = atoi( tag->findAttribute( "requests" ).c_str() );
487  if( serverRequests < m_maxOpenRequests )
488  {
489  m_maxOpenRequests = serverRequests;
490  m_logInstance.dbg( LogAreaClassConnectionBOSH,
491  "bosh parameter 'requests' now set to " + tag->findAttribute( "requests" ) );
492  }
493  }
494  if( tag->hasAttribute( "hold" ) )
495  {
496  const int maxHold = atoi( tag->findAttribute( "hold" ).c_str() );
497  if( maxHold < m_hold )
498  {
499  m_hold = maxHold;
500  m_logInstance.dbg( LogAreaClassConnectionBOSH,
501  "bosh parameter 'hold' now set to " + tag->findAttribute( "hold" ) );
502  }
503  }
504  if( tag->hasAttribute( "wait" ) )
505  {
506  const int maxWait = atoi( tag->findAttribute( "wait" ).c_str() );
507  if( maxWait < m_wait )
508  {
509  m_wait = maxWait;
510  m_logInstance.dbg( LogAreaClassConnectionBOSH,
511  "bosh parameter 'wait' now set to " + tag->findAttribute( "wait" )
512  + " seconds" );
513  }
514  }
515  if( tag->hasAttribute( "polling" ) )
516  {
517  const int minTime = atoi( tag->findAttribute( "polling" ).c_str() );
518  m_minTimePerRequest = minTime;
519  m_logInstance.dbg( LogAreaClassConnectionBOSH,
520  "bosh parameter 'polling' now set to " + tag->findAttribute( "polling" )
521  + " seconds" );
522  }
523 
524  if( m_state < StateConnected )
525  m_handler->handleConnect( this );
526 
527  m_handler->handleReceivedData( this, "<?xml version='1.0' ?>" // FIXME move to send() so that
528  // it is more clearly a response
529  // to the initial stream opener?
530  "<stream:stream xmlns:stream='http://etherx.jabber.org/streams' "
531  "xmlns='" + XMLNS_CLIENT
532  + "' version='" + XMPP_STREAM_VERSION_MAJOR + "." + XMPP_STREAM_VERSION_MINOR
533  + "' from='" + m_server + "' id ='" + m_sid + "' xml:lang='en'>" );
534  }
535 
536  if( tag->findAttribute( "type" ) == "terminate" )
537  {
538  m_logInstance.dbg( LogAreaClassConnectionBOSH,
539  "bosh connection closed by server: " + tag->findAttribute( "condition" ) );
542  return;
543  }
544 
545  const TagList& stanzas = tag->children();
546  TagList::const_iterator it = stanzas.begin();
547  for( ; it != stanzas.end(); ++it )
548  m_handler->handleReceivedData( this, (*it)->xml() );
549  }
550 
551  ConnectionBase* ConnectionBOSH::getConnection()
552  {
553  if( m_openRequests > 0 && m_openRequests >= m_maxOpenRequests )
554  {
555  m_logInstance.warn( LogAreaClassConnectionBOSH,
556  "Too many requests already open. Cannot send." );
557  return 0;
558  }
559 
560  ConnectionBase* conn = 0;
561  switch( m_connMode )
562  {
563  case ModePipelining:
564  if( !m_activeConnections.empty() )
565  {
566  m_logInstance.dbg( LogAreaClassConnectionBOSH, "Using default connection for Pipelining." );
567  return m_activeConnections.front();
568  }
569  else if( !m_connectionPool.empty() )
570  {
571  m_logInstance.warn( LogAreaClassConnectionBOSH,
572  "Pipelining selected, but no connection open. Opening one." );
573  return activateConnection();
574  }
575  else
576  m_logInstance.warn( LogAreaClassConnectionBOSH,
577  "No available connections to pipeline on." );
578  break;
579  case ModeLegacyHTTP:
580  case ModePersistentHTTP:
581  {
582  if( !m_connectionPool.empty() )
583  {
584  m_logInstance.dbg( LogAreaClassConnectionBOSH, "LegacyHTTP/PersistentHTTP selected, "
585  "using connection from pool." );
586  return activateConnection();
587  }
588  else if( !m_activeConnections.empty() )
589  {
590  m_logInstance.dbg( LogAreaClassConnectionBOSH, "No connections in pool, creating a new one." );
591  conn = m_activeConnections.front()->newInstance();
592  conn->registerConnectionDataHandler( this );
593  m_connectionPool.push_back( conn );
594  conn->connect();
595  }
596  else
597  m_logInstance.warn( LogAreaClassConnectionBOSH,
598  "No available connections to send on." );
599  break;
600  }
601  }
602  return 0;
603  }
604 
605  ConnectionBase* ConnectionBOSH::activateConnection()
606  {
607  ConnectionBase* conn = m_connectionPool.front();
608  m_connectionPool.pop_front();
609  if( conn->state() == StateConnected )
610  {
611  m_activeConnections.push_back( conn );
612  return conn;
613  }
614 
615  m_logInstance.dbg( LogAreaClassConnectionBOSH, "Connecting pooled connection." );
616  m_connectionPool.push_back( conn );
617  conn->connect();
618  return 0;
619  }
620 
621  void ConnectionBOSH::putConnection()
622  {
623  ConnectionBase* conn = m_activeConnections.front();
624 
625  switch( m_connMode )
626  {
627  case ModeLegacyHTTP:
628  m_logInstance.dbg( LogAreaClassConnectionBOSH, "Disconnecting LegacyHTTP connection" );
629  conn->disconnect();
630  conn->cleanup(); // This is necessary
631  m_activeConnections.pop_front();
632  m_connectionPool.push_back( conn );
633  break;
634  case ModePersistentHTTP:
635  m_logInstance.dbg( LogAreaClassConnectionBOSH, "Deactivating PersistentHTTP connection" );
636  m_activeConnections.pop_front();
637  m_connectionPool.push_back( conn );
638  break;
639  case ModePipelining:
640  m_logInstance.dbg( LogAreaClassConnectionBOSH, "Keeping Pipelining connection" );
641  default:
642  break;
643  }
644  }
645 
646 }