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