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