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