001/** 002 * 003 * Copyright 2009 Jive Software. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.jivesoftware.smack.bosh; 019 020import java.io.IOException; 021import java.io.PipedReader; 022import java.io.PipedWriter; 023import java.io.Writer; 024import java.util.Locale; 025import java.util.logging.Level; 026import java.util.logging.Logger; 027 028import javax.security.sasl.SaslException; 029 030import org.jivesoftware.smack.SmackException; 031import org.jivesoftware.smack.SmackException.NotConnectedException; 032import org.jivesoftware.smack.SmackException.AlreadyLoggedInException; 033import org.jivesoftware.smack.SmackException.ConnectionException; 034import org.jivesoftware.smack.SASLAuthentication; 035import org.jivesoftware.smack.XMPPConnection; 036import org.jivesoftware.smack.ConnectionCreationListener; 037import org.jivesoftware.smack.ConnectionListener; 038import org.jivesoftware.smack.Roster; 039import org.jivesoftware.smack.XMPPException; 040import org.jivesoftware.smack.packet.Packet; 041import org.jivesoftware.smack.packet.Presence; 042import org.jivesoftware.smack.packet.Presence.Type; 043import org.jivesoftware.smack.util.StringUtils; 044import org.igniterealtime.jbosh.BOSHClient; 045import org.igniterealtime.jbosh.BOSHClientConfig; 046import org.igniterealtime.jbosh.BOSHClientConnEvent; 047import org.igniterealtime.jbosh.BOSHClientConnListener; 048import org.igniterealtime.jbosh.BOSHClientRequestListener; 049import org.igniterealtime.jbosh.BOSHClientResponseListener; 050import org.igniterealtime.jbosh.BOSHException; 051import org.igniterealtime.jbosh.BOSHMessageEvent; 052import org.igniterealtime.jbosh.BodyQName; 053import org.igniterealtime.jbosh.ComposableBody; 054 055/** 056 * Creates a connection to a XMPP server via HTTP binding. 057 * This is specified in the XEP-0206: XMPP Over BOSH. 058 * 059 * @see XMPPConnection 060 * @author Guenther Niess 061 */ 062public class XMPPBOSHConnection extends XMPPConnection { 063 private static final Logger LOGGER = Logger.getLogger(XMPPBOSHConnection.class.getName()); 064 065 /** 066 * The XMPP Over Bosh namespace. 067 */ 068 public static final String XMPP_BOSH_NS = "urn:xmpp:xbosh"; 069 070 /** 071 * The BOSH namespace from XEP-0124. 072 */ 073 public static final String BOSH_URI = "http://jabber.org/protocol/httpbind"; 074 075 /** 076 * The used BOSH client from the jbosh library. 077 */ 078 private BOSHClient client; 079 080 /** 081 * Holds the initial configuration used while creating the connection. 082 */ 083 private final BOSHConfiguration config; 084 085 // Some flags which provides some info about the current state. 086 private boolean connected = false; 087 private boolean authenticated = false; 088 private boolean anonymous = false; 089 private boolean isFirstInitialization = true; 090 private boolean wasAuthenticated = false; 091 private boolean done = false; 092 093 // The readerPipe and consumer thread are used for the debugger. 094 private PipedWriter readerPipe; 095 private Thread readerConsumer; 096 097 /** 098 * The BOSH equivalent of the stream ID which is used for DIGEST authentication. 099 */ 100 protected String authID = null; 101 102 /** 103 * The session ID for the BOSH session with the connection manager. 104 */ 105 protected String sessionID = null; 106 107 /** 108 * The full JID of the authenticated user. 109 */ 110 private String user = null; 111 112 /** 113 * Create a HTTP Binding connection to a XMPP server. 114 * 115 * @param https true if you want to use SSL 116 * (e.g. false for http://domain.lt:7070/http-bind). 117 * @param host the hostname or IP address of the connection manager 118 * (e.g. domain.lt for http://domain.lt:7070/http-bind). 119 * @param port the port of the connection manager 120 * (e.g. 7070 for http://domain.lt:7070/http-bind). 121 * @param filePath the file which is described by the URL 122 * (e.g. /http-bind for http://domain.lt:7070/http-bind). 123 * @param xmppDomain the XMPP service name 124 * (e.g. domain.lt for the user alice@domain.lt) 125 */ 126 public XMPPBOSHConnection(boolean https, String host, int port, String filePath, String xmppDomain) { 127 super(new BOSHConfiguration(https, host, port, filePath, xmppDomain)); 128 this.config = (BOSHConfiguration) getConfiguration(); 129 } 130 131 /** 132 * Create a HTTP Binding connection to a XMPP server. 133 * 134 * @param config The configuration which is used for this connection. 135 */ 136 public XMPPBOSHConnection(BOSHConfiguration config) { 137 super(config); 138 this.config = config; 139 } 140 141 @Override 142 protected void connectInternal() throws SmackException { 143 if (connected) { 144 throw new IllegalStateException("Already connected to a server."); 145 } 146 done = false; 147 try { 148 // Ensure a clean starting state 149 if (client != null) { 150 client.close(); 151 client = null; 152 } 153 sessionID = null; 154 authID = null; 155 156 // Initialize BOSH client 157 BOSHClientConfig.Builder cfgBuilder = BOSHClientConfig.Builder 158 .create(config.getURI(), config.getServiceName()); 159 if (config.isProxyEnabled()) { 160 cfgBuilder.setProxy(config.getProxyAddress(), config.getProxyPort()); 161 } 162 client = BOSHClient.create(cfgBuilder.build()); 163 164 client.addBOSHClientConnListener(new BOSHConnectionListener(this)); 165 client.addBOSHClientResponseListener(new BOSHPacketReader(this)); 166 167 // Initialize the debugger 168 if (config.isDebuggerEnabled()) { 169 initDebugger(); 170 if (isFirstInitialization) { 171 if (debugger.getReaderListener() != null) { 172 addPacketListener(debugger.getReaderListener(), null); 173 } 174 if (debugger.getWriterListener() != null) { 175 addPacketSendingListener(debugger.getWriterListener(), null); 176 } 177 } 178 } 179 180 // Send the session creation request 181 client.send(ComposableBody.builder() 182 .setNamespaceDefinition("xmpp", XMPP_BOSH_NS) 183 .setAttribute(BodyQName.createWithPrefix(XMPP_BOSH_NS, "version", "xmpp"), "1.0") 184 .build()); 185 } catch (Exception e) { 186 throw new ConnectionException(e); 187 } 188 189 // Wait for the response from the server 190 synchronized (this) { 191 if (!connected) { 192 try { 193 wait(getPacketReplyTimeout()); 194 } 195 catch (InterruptedException e) {} 196 } 197 } 198 199 // If there is no feedback, throw an remote server timeout error 200 if (!connected && !done) { 201 done = true; 202 String errorMessage = "Timeout reached for the connection to " 203 + getHost() + ":" + getPort() + "."; 204 throw new SmackException(errorMessage); 205 } 206 callConnectionConnectedListener(); 207 } 208 209 public String getConnectionID() { 210 if (!connected) { 211 return null; 212 } else if (authID != null) { 213 return authID; 214 } else { 215 return sessionID; 216 } 217 } 218 219 public String getUser() { 220 return user; 221 } 222 223 public boolean isAnonymous() { 224 return anonymous; 225 } 226 227 public boolean isAuthenticated() { 228 return authenticated; 229 } 230 231 public boolean isConnected() { 232 return connected; 233 } 234 235 public boolean isSecureConnection() { 236 // TODO: Implement SSL usage 237 return false; 238 } 239 240 public boolean isUsingCompression() { 241 // TODO: Implement compression 242 return false; 243 } 244 245 public void login(String username, String password, String resource) 246 throws XMPPException, SmackException, IOException { 247 if (!isConnected()) { 248 throw new NotConnectedException(); 249 } 250 if (authenticated) { 251 throw new AlreadyLoggedInException(); 252 } 253 // Do partial version of nameprep on the username. 254 username = username.toLowerCase(Locale.US).trim(); 255 256 if (saslAuthentication.hasNonAnonymousAuthentication()) { 257 // Authenticate using SASL 258 if (password != null) { 259 saslAuthentication.authenticate(username, password, resource); 260 } else { 261 saslAuthentication.authenticate(resource, config.getCallbackHandler()); 262 } 263 } else { 264 throw new SaslException("No non-anonymous SASL authentication mechanism available"); 265 } 266 267 String response = bindResourceAndEstablishSession(resource); 268 // Set the user. 269 if (response != null) { 270 this.user = response; 271 // Update the serviceName with the one returned by the server 272 setServiceName(StringUtils.parseServer(response)); 273 } else { 274 this.user = username + "@" + getServiceName(); 275 if (resource != null) { 276 this.user += "/" + resource; 277 } 278 } 279 280 // Indicate that we're now authenticated. 281 authenticated = true; 282 anonymous = false; 283 284 // Stores the autentication for future reconnection 285 setLoginInfo(username, password, resource); 286 287 // If debugging is enabled, change the the debug window title to include 288 // the 289 // name we are now logged-in as.l 290 if (config.isDebuggerEnabled() && debugger != null) { 291 debugger.userHasLogged(user); 292 } 293 callConnectionAuthenticatedListener(); 294 295 // Set presence to online. It is important that this is done after 296 // callConnectionAuthenticatedListener(), as this call will also 297 // eventually load the roster. And we should load the roster before we 298 // send the initial presence. 299 if (config.isSendPresence()) { 300 sendPacket(new Presence(Presence.Type.available)); 301 } 302 } 303 304 public void loginAnonymously() throws XMPPException, SmackException, IOException { 305 if (!isConnected()) { 306 throw new NotConnectedException(); 307 } 308 if (authenticated) { 309 throw new AlreadyLoggedInException(); 310 } 311 312 if (saslAuthentication.hasAnonymousAuthentication()) { 313 saslAuthentication.authenticateAnonymously(); 314 } 315 else { 316 // Authenticate using Non-SASL 317 throw new SaslException("No anonymous SASL authentication mechanism available"); 318 } 319 320 String response = bindResourceAndEstablishSession(null); 321 // Set the user value. 322 this.user = response; 323 // Update the serviceName with the one returned by the server 324 setServiceName(StringUtils.parseServer(response)); 325 326 // Set presence to online. 327 if (config.isSendPresence()) { 328 sendPacket(new Presence(Presence.Type.available)); 329 } 330 331 // Indicate that we're now authenticated. 332 authenticated = true; 333 anonymous = true; 334 335 // If debugging is enabled, change the the debug window title to include the 336 // name we are now logged-in as. 337 // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger 338 // will be null 339 if (config.isDebuggerEnabled() && debugger != null) { 340 debugger.userHasLogged(user); 341 } 342 callConnectionAuthenticatedListener(); 343 } 344 345 @Override 346 protected void sendPacketInternal(Packet packet) throws NotConnectedException { 347 if (done) { 348 throw new NotConnectedException(); 349 } 350 try { 351 send(ComposableBody.builder().setPayloadXML(packet.toXML().toString()).build()); 352 } 353 catch (BOSHException e) { 354 LOGGER.log(Level.SEVERE, "BOSHException in sendPacketInternal", e); 355 } 356 } 357 358 /** 359 * Closes the connection by setting presence to unavailable and closing the 360 * HTTP client. The shutdown logic will be used during a planned disconnection or when 361 * dealing with an unexpected disconnection. Unlike {@link #disconnect()} the connection's 362 * BOSH packet reader and {@link Roster} will not be removed; thus 363 * connection's state is kept. 364 * 365 */ 366 @Override 367 protected void shutdown() { 368 setWasAuthenticated(authenticated); 369 authID = null; 370 sessionID = null; 371 done = true; 372 authenticated = false; 373 connected = false; 374 isFirstInitialization = false; 375 376 Presence unavailablePresence = new Presence(Type.unavailable); 377 try { 378 client.disconnect(ComposableBody.builder() 379 .setNamespaceDefinition("xmpp", XMPP_BOSH_NS) 380 .setPayloadXML(unavailablePresence.toXML().toString()) 381 .build()); 382 // Wait 150 ms for processes to clean-up, then shutdown. 383 Thread.sleep(150); 384 } 385 catch (Exception e) { 386 // Ignore. 387 } 388 389 // Close down the readers and writers. 390 if (readerPipe != null) { 391 try { 392 readerPipe.close(); 393 } 394 catch (Throwable ignore) { /* ignore */ } 395 reader = null; 396 } 397 if (reader != null) { 398 try { 399 reader.close(); 400 } 401 catch (Throwable ignore) { /* ignore */ } 402 reader = null; 403 } 404 if (writer != null) { 405 try { 406 writer.close(); 407 } 408 catch (Throwable ignore) { /* ignore */ } 409 writer = null; 410 } 411 412 readerConsumer = null; 413 } 414 415 /** 416 * Send a HTTP request to the connection manager with the provided body element. 417 * 418 * @param body the body which will be sent. 419 */ 420 protected void send(ComposableBody body) throws BOSHException { 421 if (!connected) { 422 throw new IllegalStateException("Not connected to a server!"); 423 } 424 if (body == null) { 425 throw new NullPointerException("Body mustn't be null!"); 426 } 427 if (sessionID != null) { 428 body = body.rebuild().setAttribute( 429 BodyQName.create(BOSH_URI, "sid"), sessionID).build(); 430 } 431 client.send(body); 432 } 433 434 /** 435 * Initialize the SmackDebugger which allows to log and debug XML traffic. 436 */ 437 protected void initDebugger() { 438 // TODO: Maybe we want to extend the SmackDebugger for simplification 439 // and a performance boost. 440 441 // Initialize a empty writer which discards all data. 442 writer = new Writer() { 443 public void write(char[] cbuf, int off, int len) { /* ignore */} 444 public void close() { /* ignore */ } 445 public void flush() { /* ignore */ } 446 }; 447 448 // Initialize a pipe for received raw data. 449 try { 450 readerPipe = new PipedWriter(); 451 reader = new PipedReader(readerPipe); 452 } 453 catch (IOException e) { 454 // Ignore 455 } 456 457 // Call the method from the parent class which initializes the debugger. 458 super.initDebugger(); 459 460 // Add listeners for the received and sent raw data. 461 client.addBOSHClientResponseListener(new BOSHClientResponseListener() { 462 public void responseReceived(BOSHMessageEvent event) { 463 if (event.getBody() != null) { 464 try { 465 readerPipe.write(event.getBody().toXML()); 466 readerPipe.flush(); 467 } catch (Exception e) { 468 // Ignore 469 } 470 } 471 } 472 }); 473 client.addBOSHClientRequestListener(new BOSHClientRequestListener() { 474 public void requestSent(BOSHMessageEvent event) { 475 if (event.getBody() != null) { 476 try { 477 writer.write(event.getBody().toXML()); 478 } catch (Exception e) { 479 // Ignore 480 } 481 } 482 } 483 }); 484 485 // Create and start a thread which discards all read data. 486 readerConsumer = new Thread() { 487 private Thread thread = this; 488 private int bufferLength = 1024; 489 490 public void run() { 491 try { 492 char[] cbuf = new char[bufferLength]; 493 while (readerConsumer == thread && !done) { 494 reader.read(cbuf, 0, bufferLength); 495 } 496 } catch (IOException e) { 497 // Ignore 498 } 499 } 500 }; 501 readerConsumer.setDaemon(true); 502 readerConsumer.start(); 503 } 504 505 /** 506 * Sends out a notification that there was an error with the connection 507 * and closes the connection. 508 * 509 * @param e the exception that causes the connection close event. 510 */ 511 protected void notifyConnectionError(Exception e) { 512 // Closes the connection temporary. A reconnection is possible 513 shutdown(); 514 callConnectionClosedOnErrorListener(e); 515 } 516 517 @Override 518 protected void processPacket(Packet packet) { 519 super.processPacket(packet); 520 } 521 522 @Override 523 protected SASLAuthentication getSASLAuthentication() { 524 return super.getSASLAuthentication(); 525 } 526 527 @Override 528 protected void serverRequiresBinding() { 529 super.serverRequiresBinding(); 530 } 531 532 @Override 533 protected void serverSupportsSession() { 534 super.serverSupportsSession(); 535 } 536 537 @Override 538 protected void serverSupportsAccountCreation() { 539 super.serverSupportsAccountCreation(); 540 } 541 542 /** 543 * A listener class which listen for a successfully established connection 544 * and connection errors and notifies the BOSHConnection. 545 * 546 * @author Guenther Niess 547 */ 548 private class BOSHConnectionListener implements BOSHClientConnListener { 549 550 private final XMPPBOSHConnection connection; 551 552 public BOSHConnectionListener(XMPPBOSHConnection connection) { 553 this.connection = connection; 554 } 555 556 /** 557 * Notify the BOSHConnection about connection state changes. 558 * Process the connection listeners and try to login if the 559 * connection was formerly authenticated and is now reconnected. 560 */ 561 public void connectionEvent(BOSHClientConnEvent connEvent) { 562 try { 563 if (connEvent.isConnected()) { 564 connected = true; 565 if (isFirstInitialization) { 566 isFirstInitialization = false; 567 for (ConnectionCreationListener listener : getConnectionCreationListeners()) { 568 listener.connectionCreated(connection); 569 } 570 } 571 else { 572 try { 573 if (wasAuthenticated) { 574 connection.login( 575 config.getUsername(), 576 config.getPassword(), 577 config.getResource()); 578 } 579 for (ConnectionListener listener : getConnectionListeners()) { 580 listener.reconnectionSuccessful(); 581 } 582 } 583 catch (Exception e) { 584 for (ConnectionListener listener : getConnectionListeners()) { 585 listener.reconnectionFailed(e); 586 } 587 } 588 } 589 } 590 else { 591 if (connEvent.isError()) { 592 // TODO Check why jbosh's getCause returns Throwable here. This is very 593 // unusual and should be avoided if possible 594 Throwable cause = connEvent.getCause(); 595 Exception e; 596 if (cause instanceof Exception) { 597 e = (Exception) cause; 598 } else { 599 e = new Exception(cause); 600 } 601 notifyConnectionError(e); 602 } 603 connected = false; 604 } 605 } 606 finally { 607 synchronized (connection) { 608 connection.notifyAll(); 609 } 610 } 611 } 612 } 613}