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.StringReader; 024import java.io.Writer; 025import java.util.logging.Level; 026import java.util.logging.Logger; 027 028import org.jivesoftware.smack.AbstractXMPPConnection; 029import org.jivesoftware.smack.SmackException; 030import org.jivesoftware.smack.SmackException.NotConnectedException; 031import org.jivesoftware.smack.SmackException.ConnectionException; 032import org.jivesoftware.smack.XMPPException.StreamErrorException; 033import org.jivesoftware.smack.XMPPConnection; 034import org.jivesoftware.smack.ConnectionCreationListener; 035import org.jivesoftware.smack.XMPPException; 036import org.jivesoftware.smack.packet.Element; 037import org.jivesoftware.smack.packet.IQ; 038import org.jivesoftware.smack.packet.Message; 039import org.jivesoftware.smack.packet.Stanza; 040import org.jivesoftware.smack.packet.PlainStreamElement; 041import org.jivesoftware.smack.packet.Presence; 042import org.jivesoftware.smack.sasl.packet.SaslStreamElements.SASLFailure; 043import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Success; 044import org.jivesoftware.smack.util.PacketParserUtils; 045import org.xmlpull.v1.XmlPullParser; 046import org.xmlpull.v1.XmlPullParserFactory; 047import org.igniterealtime.jbosh.AbstractBody; 048import org.igniterealtime.jbosh.BOSHClient; 049import org.igniterealtime.jbosh.BOSHClientConfig; 050import org.igniterealtime.jbosh.BOSHClientConnEvent; 051import org.igniterealtime.jbosh.BOSHClientConnListener; 052import org.igniterealtime.jbosh.BOSHClientRequestListener; 053import org.igniterealtime.jbosh.BOSHClientResponseListener; 054import org.igniterealtime.jbosh.BOSHException; 055import org.igniterealtime.jbosh.BOSHMessageEvent; 056import org.igniterealtime.jbosh.BodyQName; 057import org.igniterealtime.jbosh.ComposableBody; 058 059/** 060 * Creates a connection to an XMPP server via HTTP binding. 061 * This is specified in the XEP-0206: XMPP Over BOSH. 062 * 063 * @see XMPPConnection 064 * @author Guenther Niess 065 */ 066public class XMPPBOSHConnection extends AbstractXMPPConnection { 067 private static final Logger LOGGER = Logger.getLogger(XMPPBOSHConnection.class.getName()); 068 069 /** 070 * The XMPP Over Bosh namespace. 071 */ 072 public static final String XMPP_BOSH_NS = "urn:xmpp:xbosh"; 073 074 /** 075 * The BOSH namespace from XEP-0124. 076 */ 077 public static final String BOSH_URI = "http://jabber.org/protocol/httpbind"; 078 079 /** 080 * The used BOSH client from the jbosh library. 081 */ 082 private BOSHClient client; 083 084 /** 085 * Holds the initial configuration used while creating the connection. 086 */ 087 private final BOSHConfiguration config; 088 089 // Some flags which provides some info about the current state. 090 private boolean isFirstInitialization = true; 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 session ID for the BOSH session with the connection manager. 099 */ 100 protected String sessionID = null; 101 102 /** 103 * Create a HTTP Binding connection to an XMPP server. 104 * 105 * @param username the username to use. 106 * @param password the password to use. 107 * @param https true if you want to use SSL 108 * (e.g. false for http://domain.lt:7070/http-bind). 109 * @param host the hostname or IP address of the connection manager 110 * (e.g. domain.lt for http://domain.lt:7070/http-bind). 111 * @param port the port of the connection manager 112 * (e.g. 7070 for http://domain.lt:7070/http-bind). 113 * @param filePath the file which is described by the URL 114 * (e.g. /http-bind for http://domain.lt:7070/http-bind). 115 * @param xmppDomain the XMPP service name 116 * (e.g. domain.lt for the user alice@domain.lt) 117 */ 118 public XMPPBOSHConnection(String username, String password, boolean https, String host, int port, String filePath, String xmppDomain) { 119 this(BOSHConfiguration.builder().setUseHttps(https).setHost(host) 120 .setPort(port).setFile(filePath).setServiceName(xmppDomain) 121 .setUsernameAndPassword(username, password).build()); 122 } 123 124 /** 125 * Create a HTTP Binding connection to an XMPP server. 126 * 127 * @param config The configuration which is used for this connection. 128 */ 129 public XMPPBOSHConnection(BOSHConfiguration config) { 130 super(config); 131 this.config = config; 132 } 133 134 @Override 135 protected void connectInternal() throws SmackException { 136 done = false; 137 try { 138 // Ensure a clean starting state 139 if (client != null) { 140 client.close(); 141 client = null; 142 } 143 sessionID = null; 144 145 // Initialize BOSH client 146 BOSHClientConfig.Builder cfgBuilder = BOSHClientConfig.Builder 147 .create(config.getURI(), config.getServiceName()); 148 if (config.isProxyEnabled()) { 149 cfgBuilder.setProxy(config.getProxyAddress(), config.getProxyPort()); 150 } 151 client = BOSHClient.create(cfgBuilder.build()); 152 153 client.addBOSHClientConnListener(new BOSHConnectionListener()); 154 client.addBOSHClientResponseListener(new BOSHPacketReader()); 155 156 // Initialize the debugger 157 if (config.isDebuggerEnabled()) { 158 initDebugger(); 159 if (isFirstInitialization) { 160 if (debugger.getReaderListener() != null) { 161 addAsyncStanzaListener(debugger.getReaderListener(), null); 162 } 163 if (debugger.getWriterListener() != null) { 164 addPacketSendingListener(debugger.getWriterListener(), null); 165 } 166 } 167 } 168 169 // Send the session creation request 170 client.send(ComposableBody.builder() 171 .setNamespaceDefinition("xmpp", XMPP_BOSH_NS) 172 .setAttribute(BodyQName.createWithPrefix(XMPP_BOSH_NS, "version", "xmpp"), "1.0") 173 .build()); 174 } catch (Exception e) { 175 throw new ConnectionException(e); 176 } 177 178 // Wait for the response from the server 179 synchronized (this) { 180 if (!connected) { 181 try { 182 wait(getPacketReplyTimeout()); 183 } 184 catch (InterruptedException e) {} 185 } 186 } 187 188 // If there is no feedback, throw an remote server timeout error 189 if (!connected && !done) { 190 done = true; 191 String errorMessage = "Timeout reached for the connection to " 192 + getHost() + ":" + getPort() + "."; 193 throw new SmackException(errorMessage); 194 } 195 196 // Wait with SASL auth until the SASL mechanisms have been received 197 saslFeatureReceived.checkIfSuccessOrWaitOrThrow(); 198 199 callConnectionConnectedListener(); 200 } 201 202 public boolean isSecureConnection() { 203 // TODO: Implement SSL usage 204 return false; 205 } 206 207 public boolean isUsingCompression() { 208 // TODO: Implement compression 209 return false; 210 } 211 212 @Override 213 protected void loginNonAnonymously(String username, String password, String resource) 214 throws XMPPException, SmackException, IOException { 215 if (saslAuthentication.hasNonAnonymousAuthentication()) { 216 // Authenticate using SASL 217 if (password != null) { 218 saslAuthentication.authenticate(username, password, resource); 219 } else { 220 saslAuthentication.authenticate(resource, config.getCallbackHandler()); 221 } 222 } else { 223 throw new SmackException("No non-anonymous SASL authentication mechanism available"); 224 } 225 226 bindResourceAndEstablishSession(resource); 227 228 afterSuccessfulLogin(false); 229 } 230 231 @Override 232 protected void loginAnonymously() throws XMPPException, SmackException, IOException { 233 // Wait with SASL auth until the SASL mechanisms have been received 234 saslFeatureReceived.checkIfSuccessOrWaitOrThrow(); 235 236 if (saslAuthentication.hasAnonymousAuthentication()) { 237 saslAuthentication.authenticateAnonymously(); 238 } 239 else { 240 // Authenticate using Non-SASL 241 throw new SmackException("No anonymous SASL authentication mechanism available"); 242 } 243 244 bindResourceAndEstablishSession(null); 245 246 afterSuccessfulLogin(false); 247 } 248 249 @Override 250 public void send(PlainStreamElement element) throws NotConnectedException { 251 if (done) { 252 throw new NotConnectedException(); 253 } 254 sendElement(element); 255 } 256 257 @Override 258 protected void sendStanzaInternal(Stanza packet) throws NotConnectedException { 259 sendElement(packet); 260 } 261 262 private void sendElement(Element element) { 263 try { 264 send(ComposableBody.builder().setPayloadXML(element.toXML().toString()).build()); 265 if (element instanceof Stanza) { 266 firePacketSendingListeners((Stanza) element); 267 } 268 } 269 catch (BOSHException e) { 270 LOGGER.log(Level.SEVERE, "BOSHException in sendStanzaInternal", e); 271 } 272 } 273 274 /** 275 * Closes the connection by setting presence to unavailable and closing the 276 * HTTP client. The shutdown logic will be used during a planned disconnection or when 277 * dealing with an unexpected disconnection. Unlike {@link #disconnect()} the connection's 278 * BOSH stanza(/packet) reader will not be removed; thus connection's state is kept. 279 * 280 */ 281 @Override 282 protected void shutdown() { 283 setWasAuthenticated(); 284 sessionID = null; 285 done = true; 286 authenticated = false; 287 connected = false; 288 isFirstInitialization = false; 289 290 // Close down the readers and writers. 291 if (readerPipe != null) { 292 try { 293 readerPipe.close(); 294 } 295 catch (Throwable ignore) { /* ignore */ } 296 reader = null; 297 } 298 if (reader != null) { 299 try { 300 reader.close(); 301 } 302 catch (Throwable ignore) { /* ignore */ } 303 reader = null; 304 } 305 if (writer != null) { 306 try { 307 writer.close(); 308 } 309 catch (Throwable ignore) { /* ignore */ } 310 writer = null; 311 } 312 313 readerConsumer = null; 314 } 315 316 /** 317 * Send a HTTP request to the connection manager with the provided body element. 318 * 319 * @param body the body which will be sent. 320 */ 321 protected void send(ComposableBody body) throws BOSHException { 322 if (!connected) { 323 throw new IllegalStateException("Not connected to a server!"); 324 } 325 if (body == null) { 326 throw new NullPointerException("Body mustn't be null!"); 327 } 328 if (sessionID != null) { 329 body = body.rebuild().setAttribute( 330 BodyQName.create(BOSH_URI, "sid"), sessionID).build(); 331 } 332 client.send(body); 333 } 334 335 /** 336 * Initialize the SmackDebugger which allows to log and debug XML traffic. 337 */ 338 protected void initDebugger() { 339 // TODO: Maybe we want to extend the SmackDebugger for simplification 340 // and a performance boost. 341 342 // Initialize a empty writer which discards all data. 343 writer = new Writer() { 344 public void write(char[] cbuf, int off, int len) { /* ignore */} 345 public void close() { /* ignore */ } 346 public void flush() { /* ignore */ } 347 }; 348 349 // Initialize a pipe for received raw data. 350 try { 351 readerPipe = new PipedWriter(); 352 reader = new PipedReader(readerPipe); 353 } 354 catch (IOException e) { 355 // Ignore 356 } 357 358 // Call the method from the parent class which initializes the debugger. 359 super.initDebugger(); 360 361 // Add listeners for the received and sent raw data. 362 client.addBOSHClientResponseListener(new BOSHClientResponseListener() { 363 public void responseReceived(BOSHMessageEvent event) { 364 if (event.getBody() != null) { 365 try { 366 readerPipe.write(event.getBody().toXML()); 367 readerPipe.flush(); 368 } catch (Exception e) { 369 // Ignore 370 } 371 } 372 } 373 }); 374 client.addBOSHClientRequestListener(new BOSHClientRequestListener() { 375 public void requestSent(BOSHMessageEvent event) { 376 if (event.getBody() != null) { 377 try { 378 writer.write(event.getBody().toXML()); 379 } catch (Exception e) { 380 // Ignore 381 } 382 } 383 } 384 }); 385 386 // Create and start a thread which discards all read data. 387 readerConsumer = new Thread() { 388 private Thread thread = this; 389 private int bufferLength = 1024; 390 391 public void run() { 392 try { 393 char[] cbuf = new char[bufferLength]; 394 while (readerConsumer == thread && !done) { 395 reader.read(cbuf, 0, bufferLength); 396 } 397 } catch (IOException e) { 398 // Ignore 399 } 400 } 401 }; 402 readerConsumer.setDaemon(true); 403 readerConsumer.start(); 404 } 405 406 /** 407 * Sends out a notification that there was an error with the connection 408 * and closes the connection. 409 * 410 * @param e the exception that causes the connection close event. 411 */ 412 protected void notifyConnectionError(Exception e) { 413 // Closes the connection temporary. A reconnection is possible 414 shutdown(); 415 callConnectionClosedOnErrorListener(e); 416 } 417 418 /** 419 * A listener class which listen for a successfully established connection 420 * and connection errors and notifies the BOSHConnection. 421 * 422 * @author Guenther Niess 423 */ 424 private class BOSHConnectionListener implements BOSHClientConnListener { 425 426 /** 427 * Notify the BOSHConnection about connection state changes. 428 * Process the connection listeners and try to login if the 429 * connection was formerly authenticated and is now reconnected. 430 */ 431 public void connectionEvent(BOSHClientConnEvent connEvent) { 432 try { 433 if (connEvent.isConnected()) { 434 connected = true; 435 if (isFirstInitialization) { 436 isFirstInitialization = false; 437 for (ConnectionCreationListener listener : getConnectionCreationListeners()) { 438 listener.connectionCreated(XMPPBOSHConnection.this); 439 } 440 } 441 else { 442 if (wasAuthenticated) { 443 try { 444 login(); 445 } 446 catch (Exception e) { 447 throw new RuntimeException(e); 448 } 449 } 450 notifyReconnection(); 451 } 452 } 453 else { 454 if (connEvent.isError()) { 455 // TODO Check why jbosh's getCause returns Throwable here. This is very 456 // unusual and should be avoided if possible 457 Throwable cause = connEvent.getCause(); 458 Exception e; 459 if (cause instanceof Exception) { 460 e = (Exception) cause; 461 } else { 462 e = new Exception(cause); 463 } 464 notifyConnectionError(e); 465 } 466 connected = false; 467 } 468 } 469 finally { 470 synchronized (XMPPBOSHConnection.this) { 471 XMPPBOSHConnection.this.notifyAll(); 472 } 473 } 474 } 475 } 476 477 /** 478 * Listens for XML traffic from the BOSH connection manager and parses it into 479 * stanza(/packet) objects. 480 * 481 * @author Guenther Niess 482 */ 483 private class BOSHPacketReader implements BOSHClientResponseListener { 484 485 /** 486 * Parse the received packets and notify the corresponding connection. 487 * 488 * @param event the BOSH client response which includes the received packet. 489 */ 490 public void responseReceived(BOSHMessageEvent event) { 491 AbstractBody body = event.getBody(); 492 if (body != null) { 493 try { 494 if (sessionID == null) { 495 sessionID = body.getAttribute(BodyQName.create(XMPPBOSHConnection.BOSH_URI, "sid")); 496 } 497 if (streamId == null) { 498 streamId = body.getAttribute(BodyQName.create(XMPPBOSHConnection.BOSH_URI, "authid")); 499 } 500 final XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser(); 501 parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); 502 parser.setInput(new StringReader(body.toXML())); 503 int eventType = parser.getEventType(); 504 do { 505 eventType = parser.next(); 506 switch (eventType) { 507 case XmlPullParser.START_TAG: 508 String name = parser.getName(); 509 switch (name) { 510 case Message.ELEMENT: 511 case IQ.IQ_ELEMENT: 512 case Presence.ELEMENT: 513 parseAndProcessStanza(parser); 514 break; 515 case "challenge": 516 // The server is challenging the SASL authentication 517 // made by the client 518 final String challengeData = parser.nextText(); 519 getSASLAuthentication().challengeReceived(challengeData); 520 break; 521 case "success": 522 send(ComposableBody.builder().setNamespaceDefinition("xmpp", 523 XMPPBOSHConnection.XMPP_BOSH_NS).setAttribute( 524 BodyQName.createWithPrefix(XMPPBOSHConnection.XMPP_BOSH_NS, "restart", 525 "xmpp"), "true").setAttribute( 526 BodyQName.create(XMPPBOSHConnection.BOSH_URI, "to"), getServiceName()).build()); 527 Success success = new Success(parser.nextText()); 528 getSASLAuthentication().authenticated(success); 529 break; 530 case "features": 531 parseFeatures(parser); 532 break; 533 case "failure": 534 if ("urn:ietf:params:xml:ns:xmpp-sasl".equals(parser.getNamespace(null))) { 535 final SASLFailure failure = PacketParserUtils.parseSASLFailure(parser); 536 getSASLAuthentication().authenticationFailed(failure); 537 } 538 break; 539 case "error": 540 throw new StreamErrorException(PacketParserUtils.parseStreamError(parser)); 541 } 542 break; 543 } 544 } 545 while (eventType != XmlPullParser.END_DOCUMENT); 546 } 547 catch (Exception e) { 548 if (isConnected()) { 549 notifyConnectionError(e); 550 } 551 } 552 } 553 } 554 } 555}