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