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.ConnectionException; 031import org.jivesoftware.smack.SmackException.NotConnectedException; 032import org.jivesoftware.smack.XMPPConnection; 033import org.jivesoftware.smack.XMPPException; 034import org.jivesoftware.smack.XMPPException.StreamErrorException; 035import org.jivesoftware.smack.packet.Element; 036import org.jivesoftware.smack.packet.IQ; 037import org.jivesoftware.smack.packet.Message; 038import org.jivesoftware.smack.packet.Nonza; 039import org.jivesoftware.smack.packet.Presence; 040import org.jivesoftware.smack.packet.Stanza; 041import org.jivesoftware.smack.packet.StanzaError; 042import org.jivesoftware.smack.sasl.packet.SaslStreamElements.SASLFailure; 043import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Success; 044import org.jivesoftware.smack.util.PacketParserUtils; 045 046import org.igniterealtime.jbosh.AbstractBody; 047import org.igniterealtime.jbosh.BOSHClient; 048import org.igniterealtime.jbosh.BOSHClientConfig; 049import org.igniterealtime.jbosh.BOSHClientConnEvent; 050import org.igniterealtime.jbosh.BOSHClientConnListener; 051import org.igniterealtime.jbosh.BOSHClientRequestListener; 052import org.igniterealtime.jbosh.BOSHClientResponseListener; 053import org.igniterealtime.jbosh.BOSHException; 054import org.igniterealtime.jbosh.BOSHMessageEvent; 055import org.igniterealtime.jbosh.BodyQName; 056import org.igniterealtime.jbosh.ComposableBody; 057 058import org.jxmpp.jid.DomainBareJid; 059import org.jxmpp.jid.parts.Resourcepart; 060import org.xmlpull.v1.XmlPullParser; 061import org.xmlpull.v1.XmlPullParserFactory; 062 063/** 064 * Creates a connection to an XMPP server via HTTP binding. 065 * This is specified in the XEP-0206: XMPP Over BOSH. 066 * 067 * @see XMPPConnection 068 * @author Guenther Niess 069 */ 070public class XMPPBOSHConnection extends AbstractXMPPConnection { 071 private static final Logger LOGGER = Logger.getLogger(XMPPBOSHConnection.class.getName()); 072 073 /** 074 * The XMPP Over Bosh namespace. 075 */ 076 public static final String XMPP_BOSH_NS = "urn:xmpp:xbosh"; 077 078 /** 079 * The BOSH namespace from XEP-0124. 080 */ 081 public static final String BOSH_URI = "http://jabber.org/protocol/httpbind"; 082 083 /** 084 * The used BOSH client from the jbosh library. 085 */ 086 private BOSHClient client; 087 088 /** 089 * Holds the initial configuration used while creating the connection. 090 */ 091 @SuppressWarnings("HidingField") 092 private final BOSHConfiguration config; 093 094 // Some flags which provides some info about the current state. 095 private boolean isFirstInitialization = true; 096 private boolean done = false; 097 098 // The readerPipe and consumer thread are used for the debugger. 099 private PipedWriter readerPipe; 100 private Thread readerConsumer; 101 102 /** 103 * The session ID for the BOSH session with the connection manager. 104 */ 105 protected String sessionID = null; 106 107 private boolean notified; 108 109 /** 110 * Create a HTTP Binding connection to an XMPP server. 111 * 112 * @param username the username to use. 113 * @param password the password to use. 114 * @param https true if you want to use SSL 115 * (e.g. false for http://domain.lt:7070/http-bind). 116 * @param host the hostname or IP address of the connection manager 117 * (e.g. domain.lt for http://domain.lt:7070/http-bind). 118 * @param port the port of the connection manager 119 * (e.g. 7070 for http://domain.lt:7070/http-bind). 120 * @param filePath the file which is described by the URL 121 * (e.g. /http-bind for http://domain.lt:7070/http-bind). 122 * @param xmppServiceDomain the XMPP service name 123 * (e.g. domain.lt for the user alice@domain.lt) 124 */ 125 public XMPPBOSHConnection(String username, String password, boolean https, String host, int port, String filePath, DomainBareJid xmppServiceDomain) { 126 this(BOSHConfiguration.builder().setUseHttps(https).setHost(host) 127 .setPort(port).setFile(filePath).setXmppDomain(xmppServiceDomain) 128 .setUsernameAndPassword(username, password).build()); 129 } 130 131 /** 132 * Create a HTTP Binding connection to an 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, 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 client = BOSHClient.create(cfgBuilder.build()); 160 161 client.addBOSHClientConnListener(new BOSHConnectionListener()); 162 client.addBOSHClientResponseListener(new BOSHPacketReader()); 163 164 // Initialize the debugger 165 if (debugger != null) { 166 initDebugger(); 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 final long deadline = System.currentTimeMillis() + getReplyTimeout(); 182 while (!notified) { 183 final long now = System.currentTimeMillis(); 184 if (now >= deadline) break; 185 wait(deadline - now); 186 } 187 } 188 } 189 190 // If there is no feedback, throw an remote server timeout error 191 if (!connected && !done) { 192 done = true; 193 String errorMessage = "Timeout reached for the connection to " 194 + getHost() + ":" + getPort() + "."; 195 throw new SmackException(errorMessage); 196 } 197 } 198 199 @Override 200 public boolean isSecureConnection() { 201 // TODO: Implement SSL usage 202 return false; 203 } 204 205 @Override 206 public boolean isUsingCompression() { 207 // TODO: Implement compression 208 return false; 209 } 210 211 @Override 212 protected void loginInternal(String username, String password, Resourcepart resource) throws XMPPException, 213 SmackException, IOException, InterruptedException { 214 // Authenticate using SASL 215 saslAuthentication.authenticate(username, password, config.getAuthzid(), null); 216 217 bindResourceAndEstablishSession(resource); 218 219 afterSuccessfulLogin(false); 220 } 221 222 @Override 223 public void sendNonza(Nonza element) throws NotConnectedException { 224 if (done) { 225 throw new NotConnectedException(); 226 } 227 sendElement(element); 228 } 229 230 @Override 231 protected void sendStanzaInternal(Stanza packet) throws NotConnectedException { 232 sendElement(packet); 233 } 234 235 private void sendElement(Element element) { 236 try { 237 send(ComposableBody.builder().setPayloadXML(element.toXML(BOSH_URI).toString()).build()); 238 if (element instanceof Stanza) { 239 firePacketSendingListeners((Stanza) element); 240 } 241 } 242 catch (BOSHException e) { 243 LOGGER.log(Level.SEVERE, "BOSHException in sendStanzaInternal", e); 244 } 245 } 246 247 /** 248 * Closes the connection by setting presence to unavailable and closing the 249 * HTTP client. The shutdown logic will be used during a planned disconnection or when 250 * dealing with an unexpected disconnection. Unlike {@link #disconnect()} the connection's 251 * BOSH stanza reader will not be removed; thus connection's state is kept. 252 * 253 */ 254 @Override 255 protected void shutdown() { 256 257 if (client != null) { 258 try { 259 client.disconnect(); 260 } catch (Exception e) { 261 LOGGER.log(Level.WARNING, "shutdown", e); 262 } 263 client = null; 264 } 265 266 instantShutdown(); 267 } 268 269 @Override 270 public void instantShutdown() { 271 setWasAuthenticated(); 272 sessionID = null; 273 done = true; 274 authenticated = false; 275 connected = false; 276 isFirstInitialization = false; 277 278 // Close down the readers and writers. 279 if (readerPipe != null) { 280 try { 281 readerPipe.close(); 282 } 283 catch (Throwable ignore) { /* ignore */ } 284 reader = null; 285 } 286 if (reader != null) { 287 try { 288 reader.close(); 289 } 290 catch (Throwable ignore) { /* ignore */ } 291 reader = null; 292 } 293 if (writer != null) { 294 try { 295 writer.close(); 296 } 297 catch (Throwable ignore) { /* ignore */ } 298 writer = null; 299 } 300 301 readerConsumer = null; 302 } 303 304 /** 305 * Send a HTTP request to the connection manager with the provided body element. 306 * 307 * @param body the body which will be sent. 308 * @throws BOSHException 309 */ 310 protected void send(ComposableBody body) throws BOSHException { 311 if (!connected) { 312 throw new IllegalStateException("Not connected to a server!"); 313 } 314 if (body == null) { 315 throw new NullPointerException("Body mustn't be null!"); 316 } 317 if (sessionID != null) { 318 body = body.rebuild().setAttribute( 319 BodyQName.create(BOSH_URI, "sid"), sessionID).build(); 320 } 321 client.send(body); 322 } 323 324 /** 325 * Initialize the SmackDebugger which allows to log and debug XML traffic. 326 */ 327 @Override 328 protected void initDebugger() { 329 // TODO: Maybe we want to extend the SmackDebugger for simplification 330 // and a performance boost. 331 332 // Initialize a empty writer which discards all data. 333 writer = new Writer() { 334 @Override 335 public void write(char[] cbuf, int off, int len) { 336 /* ignore */ } 337 338 @Override 339 public void close() { 340 /* ignore */ } 341 342 @Override 343 public void flush() { 344 /* ignore */ } 345 }; 346 347 // Initialize a pipe for received raw data. 348 try { 349 readerPipe = new PipedWriter(); 350 reader = new PipedReader(readerPipe); 351 } 352 catch (IOException e) { 353 // Ignore 354 } 355 356 // Call the method from the parent class which initializes the debugger. 357 super.initDebugger(); 358 359 // Add listeners for the received and sent raw data. 360 client.addBOSHClientResponseListener(new BOSHClientResponseListener() { 361 @Override 362 public void responseReceived(BOSHMessageEvent event) { 363 if (event.getBody() != null) { 364 try { 365 readerPipe.write(event.getBody().toXML()); 366 readerPipe.flush(); 367 } catch (Exception e) { 368 // Ignore 369 } 370 } 371 } 372 }); 373 client.addBOSHClientRequestListener(new BOSHClientRequestListener() { 374 @Override 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 @Override 392 public void run() { 393 try { 394 char[] cbuf = new char[bufferLength]; 395 while (readerConsumer == thread && !done) { 396 reader.read(cbuf, 0, bufferLength); 397 } 398 } catch (IOException e) { 399 // Ignore 400 } 401 } 402 }; 403 readerConsumer.setDaemon(true); 404 readerConsumer.start(); 405 } 406 407 /** 408 * Sends out a notification that there was an error with the connection 409 * and closes the connection. 410 * 411 * @param e the exception that causes the connection close event. 412 */ 413 protected void notifyConnectionError(Exception e) { 414 // Closes the connection temporary. A reconnection is possible 415 shutdown(); 416 callConnectionClosedOnErrorListener(e); 417 } 418 419 /** 420 * A listener class which listen for a successfully established connection 421 * and connection errors and notifies the BOSHConnection. 422 * 423 * @author Guenther Niess 424 */ 425 private class BOSHConnectionListener implements BOSHClientConnListener { 426 427 /** 428 * Notify the BOSHConnection about connection state changes. 429 * Process the connection listeners and try to login if the 430 * connection was formerly authenticated and is now reconnected. 431 */ 432 @Override 433 public void connectionEvent(BOSHClientConnEvent connEvent) { 434 try { 435 if (connEvent.isConnected()) { 436 connected = true; 437 if (isFirstInitialization) { 438 isFirstInitialization = false; 439 } 440 else { 441 if (wasAuthenticated) { 442 try { 443 login(); 444 } 445 catch (Exception e) { 446 throw new RuntimeException(e); 447 } 448 } 449 } 450 } 451 else { 452 if (connEvent.isError()) { 453 // TODO Check why jbosh's getCause returns Throwable here. This is very 454 // unusual and should be avoided if possible 455 Throwable cause = connEvent.getCause(); 456 Exception e; 457 if (cause instanceof Exception) { 458 e = (Exception) cause; 459 } else { 460 e = new Exception(cause); 461 } 462 notifyConnectionError(e); 463 } 464 connected = false; 465 } 466 } 467 finally { 468 notified = true; 469 synchronized (XMPPBOSHConnection.this) { 470 XMPPBOSHConnection.this.notifyAll(); 471 } 472 } 473 } 474 } 475 476 /** 477 * Listens for XML traffic from the BOSH connection manager and parses it into 478 * stanza objects. 479 * 480 * @author Guenther Niess 481 */ 482 private class BOSHPacketReader implements BOSHClientResponseListener { 483 484 /** 485 * Parse the received packets and notify the corresponding connection. 486 * 487 * @param event the BOSH client response which includes the received packet. 488 */ 489 @Override 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"), getXMPPServiceDomain().toString()).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 // Some BOSH error isn't stream error. 541 if ("urn:ietf:params:xml:ns:xmpp-streams".equals(parser.getNamespace(null))) { 542 throw new StreamErrorException(PacketParserUtils.parseStreamError(parser)); 543 } else { 544 StanzaError.Builder builder = PacketParserUtils.parseError(parser); 545 throw new XMPPException.XMPPErrorException(null, builder.build()); 546 } 547 } 548 break; 549 } 550 } 551 while (eventType != XmlPullParser.END_DOCUMENT); 552 } 553 catch (Exception e) { 554 if (isConnected()) { 555 notifyConnectionError(e); 556 } 557 } 558 } 559 } 560 } 561}