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 // Set presence to online. 281 if (config.isSendPresence()) { 282 sendPacket(new Presence(Presence.Type.available)); 283 } 284 285 // Indicate that we're now authenticated. 286 authenticated = true; 287 anonymous = false; 288 289 // Stores the autentication for future reconnection 290 setLoginInfo(username, password, resource); 291 292 // If debugging is enabled, change the the debug window title to include 293 // the 294 // name we are now logged-in as.l 295 if (config.isDebuggerEnabled() && debugger != null) { 296 debugger.userHasLogged(user); 297 } 298 callConnectionAuthenticatedListener(); 299 } 300 301 public void loginAnonymously() throws XMPPException, SmackException, IOException { 302 if (!isConnected()) { 303 throw new NotConnectedException(); 304 } 305 if (authenticated) { 306 throw new AlreadyLoggedInException(); 307 } 308 309 if (saslAuthentication.hasAnonymousAuthentication()) { 310 saslAuthentication.authenticateAnonymously(); 311 } 312 else { 313 // Authenticate using Non-SASL 314 throw new SaslException("No anonymous SASL authentication mechanism available"); 315 } 316 317 String response = bindResourceAndEstablishSession(null); 318 // Set the user value. 319 this.user = response; 320 // Update the serviceName with the one returned by the server 321 setServiceName(StringUtils.parseServer(response)); 322 323 // Set presence to online. 324 if (config.isSendPresence()) { 325 sendPacket(new Presence(Presence.Type.available)); 326 } 327 328 // Indicate that we're now authenticated. 329 authenticated = true; 330 anonymous = true; 331 332 // If debugging is enabled, change the the debug window title to include the 333 // name we are now logged-in as. 334 // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger 335 // will be null 336 if (config.isDebuggerEnabled() && debugger != null) { 337 debugger.userHasLogged(user); 338 } 339 callConnectionAuthenticatedListener(); 340 } 341 342 @Override 343 protected void sendPacketInternal(Packet packet) throws NotConnectedException { 344 if (done) { 345 throw new NotConnectedException(); 346 } 347 try { 348 send(ComposableBody.builder().setPayloadXML(packet.toXML().toString()).build()); 349 } 350 catch (BOSHException e) { 351 LOGGER.log(Level.SEVERE, "BOSHException in sendPacketInternal", e); 352 } 353 } 354 355 /** 356 * Closes the connection by setting presence to unavailable and closing the 357 * HTTP client. The shutdown logic will be used during a planned disconnection or when 358 * dealing with an unexpected disconnection. Unlike {@link #disconnect()} the connection's 359 * BOSH packet reader and {@link Roster} will not be removed; thus 360 * connection's state is kept. 361 * 362 */ 363 @Override 364 protected void shutdown() { 365 setWasAuthenticated(authenticated); 366 authID = null; 367 sessionID = null; 368 done = true; 369 authenticated = false; 370 connected = false; 371 isFirstInitialization = false; 372 373 Presence unavailablePresence = new Presence(Type.unavailable); 374 try { 375 client.disconnect(ComposableBody.builder() 376 .setNamespaceDefinition("xmpp", XMPP_BOSH_NS) 377 .setPayloadXML(unavailablePresence.toXML().toString()) 378 .build()); 379 // Wait 150 ms for processes to clean-up, then shutdown. 380 Thread.sleep(150); 381 } 382 catch (Exception e) { 383 // Ignore. 384 } 385 386 // Close down the readers and writers. 387 if (readerPipe != null) { 388 try { 389 readerPipe.close(); 390 } 391 catch (Throwable ignore) { /* ignore */ } 392 reader = null; 393 } 394 if (reader != null) { 395 try { 396 reader.close(); 397 } 398 catch (Throwable ignore) { /* ignore */ } 399 reader = null; 400 } 401 if (writer != null) { 402 try { 403 writer.close(); 404 } 405 catch (Throwable ignore) { /* ignore */ } 406 writer = null; 407 } 408 409 readerConsumer = null; 410 } 411 412 /** 413 * Send a HTTP request to the connection manager with the provided body element. 414 * 415 * @param body the body which will be sent. 416 */ 417 protected void send(ComposableBody body) throws BOSHException { 418 if (!connected) { 419 throw new IllegalStateException("Not connected to a server!"); 420 } 421 if (body == null) { 422 throw new NullPointerException("Body mustn't be null!"); 423 } 424 if (sessionID != null) { 425 body = body.rebuild().setAttribute( 426 BodyQName.create(BOSH_URI, "sid"), sessionID).build(); 427 } 428 client.send(body); 429 } 430 431 /** 432 * Initialize the SmackDebugger which allows to log and debug XML traffic. 433 */ 434 protected void initDebugger() { 435 // TODO: Maybe we want to extend the SmackDebugger for simplification 436 // and a performance boost. 437 438 // Initialize a empty writer which discards all data. 439 writer = new Writer() { 440 public void write(char[] cbuf, int off, int len) { /* ignore */} 441 public void close() { /* ignore */ } 442 public void flush() { /* ignore */ } 443 }; 444 445 // Initialize a pipe for received raw data. 446 try { 447 readerPipe = new PipedWriter(); 448 reader = new PipedReader(readerPipe); 449 } 450 catch (IOException e) { 451 // Ignore 452 } 453 454 // Call the method from the parent class which initializes the debugger. 455 super.initDebugger(); 456 457 // Add listeners for the received and sent raw data. 458 client.addBOSHClientResponseListener(new BOSHClientResponseListener() { 459 public void responseReceived(BOSHMessageEvent event) { 460 if (event.getBody() != null) { 461 try { 462 readerPipe.write(event.getBody().toXML()); 463 readerPipe.flush(); 464 } catch (Exception e) { 465 // Ignore 466 } 467 } 468 } 469 }); 470 client.addBOSHClientRequestListener(new BOSHClientRequestListener() { 471 public void requestSent(BOSHMessageEvent event) { 472 if (event.getBody() != null) { 473 try { 474 writer.write(event.getBody().toXML()); 475 } catch (Exception e) { 476 // Ignore 477 } 478 } 479 } 480 }); 481 482 // Create and start a thread which discards all read data. 483 readerConsumer = new Thread() { 484 private Thread thread = this; 485 private int bufferLength = 1024; 486 487 public void run() { 488 try { 489 char[] cbuf = new char[bufferLength]; 490 while (readerConsumer == thread && !done) { 491 reader.read(cbuf, 0, bufferLength); 492 } 493 } catch (IOException e) { 494 // Ignore 495 } 496 } 497 }; 498 readerConsumer.setDaemon(true); 499 readerConsumer.start(); 500 } 501 502 /** 503 * Sends out a notification that there was an error with the connection 504 * and closes the connection. 505 * 506 * @param e the exception that causes the connection close event. 507 */ 508 protected void notifyConnectionError(Exception e) { 509 // Closes the connection temporary. A reconnection is possible 510 shutdown(); 511 callConnectionClosedOnErrorListener(e); 512 } 513 514 @Override 515 protected void processPacket(Packet packet) { 516 super.processPacket(packet); 517 } 518 519 @Override 520 protected SASLAuthentication getSASLAuthentication() { 521 return super.getSASLAuthentication(); 522 } 523 524 @Override 525 protected void serverRequiresBinding() { 526 super.serverRequiresBinding(); 527 } 528 529 @Override 530 protected void serverSupportsSession() { 531 super.serverSupportsSession(); 532 } 533 534 @Override 535 protected void serverSupportsAccountCreation() { 536 super.serverSupportsAccountCreation(); 537 } 538 539 /** 540 * A listener class which listen for a successfully established connection 541 * and connection errors and notifies the BOSHConnection. 542 * 543 * @author Guenther Niess 544 */ 545 private class BOSHConnectionListener implements BOSHClientConnListener { 546 547 private final XMPPBOSHConnection connection; 548 549 public BOSHConnectionListener(XMPPBOSHConnection connection) { 550 this.connection = connection; 551 } 552 553 /** 554 * Notify the BOSHConnection about connection state changes. 555 * Process the connection listeners and try to login if the 556 * connection was formerly authenticated and is now reconnected. 557 */ 558 public void connectionEvent(BOSHClientConnEvent connEvent) { 559 try { 560 if (connEvent.isConnected()) { 561 connected = true; 562 if (isFirstInitialization) { 563 isFirstInitialization = false; 564 for (ConnectionCreationListener listener : getConnectionCreationListeners()) { 565 listener.connectionCreated(connection); 566 } 567 } 568 else { 569 try { 570 if (wasAuthenticated) { 571 connection.login( 572 config.getUsername(), 573 config.getPassword(), 574 config.getResource()); 575 } 576 for (ConnectionListener listener : getConnectionListeners()) { 577 listener.reconnectionSuccessful(); 578 } 579 } 580 catch (Exception e) { 581 for (ConnectionListener listener : getConnectionListeners()) { 582 listener.reconnectionFailed(e); 583 } 584 } 585 } 586 } 587 else { 588 if (connEvent.isError()) { 589 // TODO Check why jbosh's getCause returns Throwable here. This is very 590 // unusual and should be avoided if possible 591 Throwable cause = connEvent.getCause(); 592 Exception e; 593 if (cause instanceof Exception) { 594 e = (Exception) cause; 595 } else { 596 e = new Exception(cause); 597 } 598 notifyConnectionError(e); 599 } 600 connected = false; 601 } 602 } 603 finally { 604 synchronized (connection) { 605 connection.notifyAll(); 606 } 607 } 608 } 609 } 610}