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