JingleConnection.java

   1package eu.siacs.conversations.xmpp.jingle;
   2
   3import android.util.Base64;
   4import android.util.Log;
   5import android.util.Pair;
   6
   7import java.io.FileInputStream;
   8import java.io.FileNotFoundException;
   9import java.io.IOException;
  10import java.io.InputStream;
  11import java.io.OutputStream;
  12import java.util.ArrayList;
  13import java.util.Arrays;
  14import java.util.Iterator;
  15import java.util.List;
  16import java.util.Locale;
  17import java.util.Map.Entry;
  18import java.util.concurrent.ConcurrentHashMap;
  19
  20import eu.siacs.conversations.Config;
  21import eu.siacs.conversations.crypto.axolotl.AxolotlService;
  22import eu.siacs.conversations.crypto.axolotl.OnMessageCreatedCallback;
  23import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
  24import eu.siacs.conversations.entities.Account;
  25import eu.siacs.conversations.entities.Conversation;
  26import eu.siacs.conversations.entities.DownloadableFile;
  27import eu.siacs.conversations.entities.Message;
  28import eu.siacs.conversations.entities.Presence;
  29import eu.siacs.conversations.entities.ServiceDiscoveryResult;
  30import eu.siacs.conversations.entities.Transferable;
  31import eu.siacs.conversations.entities.TransferablePlaceholder;
  32import eu.siacs.conversations.parser.IqParser;
  33import eu.siacs.conversations.persistance.FileBackend;
  34import eu.siacs.conversations.services.AbstractConnectionManager;
  35import eu.siacs.conversations.services.XmppConnectionService;
  36import eu.siacs.conversations.utils.CryptoHelper;
  37import eu.siacs.conversations.xml.Element;
  38import eu.siacs.conversations.xmpp.OnIqPacketReceived;
  39import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
  40import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
  41import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
  42import eu.siacs.conversations.xmpp.stanzas.IqPacket;
  43import rocks.xmpp.addr.Jid;
  44
  45public class JingleConnection implements Transferable {
  46
  47	private JingleConnectionManager mJingleConnectionManager;
  48	private XmppConnectionService mXmppConnectionService;
  49
  50	private static final int JINGLE_STATUS_OFFERED = -1;
  51	protected static final int JINGLE_STATUS_INITIATED = 0;
  52	protected static final int JINGLE_STATUS_ACCEPTED = 1;
  53	protected static final int JINGLE_STATUS_FINISHED = 4;
  54	protected static final int JINGLE_STATUS_TRANSMITTING = 5;
  55	protected static final int JINGLE_STATUS_FAILED = 99;
  56
  57	private Content.Version ftVersion = Content.Version.FT_3;
  58
  59	private int ibbBlockSize = 8192;
  60
  61	private int mJingleStatus = JINGLE_STATUS_OFFERED;
  62	private int mStatus = Transferable.STATUS_UNKNOWN;
  63	private Message message;
  64	private String sessionId;
  65	private Account account;
  66	private Jid initiator;
  67	private Jid responder;
  68	private List<JingleCandidate> candidates = new ArrayList<>();
  69	private ConcurrentHashMap<String, JingleSocks5Transport> connections = new ConcurrentHashMap<>();
  70
  71	private String transportId;
  72	private Element fileOffer;
  73	private DownloadableFile file = null;
  74
  75	private String contentName;
  76	private String contentCreator;
  77
  78	private int mProgress = 0;
  79
  80	private boolean receivedCandidate = false;
  81	private boolean sentCandidate = false;
  82
  83	private boolean acceptedAutomatically = false;
  84	private boolean cancelled = false;
  85
  86	private XmppAxolotlMessage mXmppAxolotlMessage;
  87
  88	private JingleTransport transport = null;
  89
  90	private OutputStream mFileOutputStream;
  91	private InputStream mFileInputStream;
  92
  93	private OnIqPacketReceived responseListener = (account, packet) -> {
  94		if (packet.getType() != IqPacket.TYPE.RESULT) {
  95			fail(IqParser.extractErrorMessage(packet));
  96		}
  97	};
  98	private byte[] expectedHash = new byte[0];
  99
 100	private boolean responding() {
 101		return responder != null && responder.equals(account.getJid());
 102	}
 103
 104	private boolean initiating() {
 105		return initiator.equals(account.getJid());
 106	}
 107
 108	final OnFileTransmissionStatusChanged onFileTransmissionStatusChanged = new OnFileTransmissionStatusChanged() {
 109
 110		@Override
 111		public void onFileTransmitted(DownloadableFile file) {
 112			if (responding()) {
 113				if (expectedHash.length > 0 && !Arrays.equals(expectedHash,file.getSha1Sum())) {
 114					Log.d(Config.LOGTAG,account.getJid().asBareJid()+": hashes did not match");
 115				}
 116				sendSuccess();
 117				mXmppConnectionService.getFileBackend().updateFileParams(message);
 118				mXmppConnectionService.databaseBackend.createMessage(message);
 119				mXmppConnectionService.markMessage(message,Message.STATUS_RECEIVED);
 120				if (acceptedAutomatically) {
 121					message.markUnread();
 122					if (message.getEncryption() == Message.ENCRYPTION_PGP) {
 123						account.getPgpDecryptionService().decrypt(message, true);
 124					} else {
 125						mXmppConnectionService.getFileBackend().updateMediaScanner(file, () -> JingleConnection.this.mXmppConnectionService.getNotificationService().push(message));
 126
 127					}
 128					Log.d(Config.LOGTAG,"successfully transmitted file:" + file.getAbsolutePath()+" ("+ CryptoHelper.bytesToHex(file.getSha1Sum())+")");
 129					return;
 130				}
 131			} else {
 132				if (ftVersion == Content.Version.FT_5) { //older Conversations will break when receiving a session-info
 133					sendHash();
 134				}
 135				if (message.getEncryption() == Message.ENCRYPTION_PGP) {
 136					account.getPgpDecryptionService().decrypt(message, false);
 137				}
 138				if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 139					file.delete();
 140				}
 141			}
 142			Log.d(Config.LOGTAG,"successfully transmitted file:" + file.getAbsolutePath()+" ("+ CryptoHelper.bytesToHex(file.getSha1Sum())+")");
 143			if (message.getEncryption() != Message.ENCRYPTION_PGP) {
 144				mXmppConnectionService.getFileBackend().updateMediaScanner(file);
 145			}
 146		}
 147
 148		@Override
 149		public void onFileTransferAborted() {
 150			JingleConnection.this.sendCancel();
 151			JingleConnection.this.fail();
 152		}
 153	};
 154
 155	InputStream getFileInputStream() {
 156		return this.mFileInputStream;
 157	}
 158
 159	OutputStream getFileOutputStream() throws IOException {
 160		if (this.file == null) {
 161			Log.d(Config.LOGTAG,"file object was not assigned");
 162			return null;
 163		}
 164		this.file.getParentFile().mkdirs();
 165		this.file.createNewFile();
 166		this.mFileOutputStream = AbstractConnectionManager.createOutputStream(this.file);
 167		return this.mFileOutputStream;
 168	}
 169
 170	private OnProxyActivated onProxyActivated = new OnProxyActivated() {
 171
 172		@Override
 173		public void success() {
 174			if (initiator.equals(account.getJid())) {
 175				Log.d(Config.LOGTAG, "we were initiating. sending file");
 176				transport.send(file, onFileTransmissionStatusChanged);
 177			} else {
 178				transport.receive(file, onFileTransmissionStatusChanged);
 179				Log.d(Config.LOGTAG, "we were responding. receiving file");
 180			}
 181		}
 182
 183		@Override
 184		public void failed() {
 185			Log.d(Config.LOGTAG, "proxy activation failed");
 186		}
 187	};
 188
 189	public JingleConnection(JingleConnectionManager mJingleConnectionManager) {
 190		this.mJingleConnectionManager = mJingleConnectionManager;
 191		this.mXmppConnectionService = mJingleConnectionManager
 192				.getXmppConnectionService();
 193	}
 194
 195	public String getSessionId() {
 196		return this.sessionId;
 197	}
 198
 199	public Account getAccount() {
 200		return this.account;
 201	}
 202
 203	public Jid getCounterPart() {
 204		return this.message.getCounterpart();
 205	}
 206
 207	public void deliverPacket(JinglePacket packet) {
 208		boolean returnResult = true;
 209		if (packet.isAction("session-terminate")) {
 210			Reason reason = packet.getReason();
 211			if (reason != null) {
 212				if (reason.hasChild("cancel")) {
 213					this.fail();
 214				} else if (reason.hasChild("success")) {
 215					this.receiveSuccess();
 216				} else {
 217					this.fail();
 218				}
 219			} else {
 220				this.fail();
 221			}
 222		} else if (packet.isAction("session-accept")) {
 223			returnResult = receiveAccept(packet);
 224		} else if (packet.isAction("session-info")) {
 225			returnResult = true;
 226			Element checksum = packet.getChecksum();
 227			Element file = checksum == null ? null : checksum.findChild("file");
 228			Element hash = file == null ? null : file.findChild("hash","urn:xmpp:hashes:2");
 229			if (hash != null && "sha-1".equalsIgnoreCase(hash.getAttribute("algo"))) {
 230				try {
 231					this.expectedHash = Base64.decode(hash.getContent(), Base64.DEFAULT);
 232				} catch (Exception e) {
 233					this.expectedHash = new byte[0];
 234				}
 235			}
 236		} else if (packet.isAction("transport-info")) {
 237			returnResult = receiveTransportInfo(packet);
 238		} else if (packet.isAction("transport-replace")) {
 239			if (packet.getJingleContent().hasIbbTransport()) {
 240				returnResult = this.receiveFallbackToIbb(packet);
 241			} else {
 242				returnResult = false;
 243				Log.d(Config.LOGTAG, "trying to fallback to something unknown"
 244						+ packet.toString());
 245			}
 246		} else if (packet.isAction("transport-accept")) {
 247			returnResult = this.receiveTransportAccept(packet);
 248		} else {
 249			Log.d(Config.LOGTAG, "packet arrived in connection. action was "
 250					+ packet.getAction());
 251			returnResult = false;
 252		}
 253		IqPacket response;
 254		if (returnResult) {
 255			response = packet.generateResponse(IqPacket.TYPE.RESULT);
 256
 257		} else {
 258			response = packet.generateResponse(IqPacket.TYPE.ERROR);
 259		}
 260		mXmppConnectionService.sendIqPacket(account,response,null);
 261	}
 262
 263	public void init(final Message message) {
 264		if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
 265			Conversation conversation = (Conversation) message.getConversation();
 266			conversation.getAccount().getAxolotlService().prepareKeyTransportMessage(conversation, new OnMessageCreatedCallback() {
 267				@Override
 268				public void run(XmppAxolotlMessage xmppAxolotlMessage) {
 269					if (xmppAxolotlMessage != null) {
 270						init(message, xmppAxolotlMessage);
 271					} else {
 272						fail();
 273					}
 274				}
 275			});
 276		} else {
 277			init(message, null);
 278		}
 279	}
 280
 281	private void init(Message message, XmppAxolotlMessage xmppAxolotlMessage) {
 282		this.mXmppAxolotlMessage = xmppAxolotlMessage;
 283		this.contentCreator = "initiator";
 284		this.contentName = this.mJingleConnectionManager.nextRandomId();
 285		this.message = message;
 286		this.account = message.getConversation().getAccount();
 287		upgradeNamespace();
 288		this.message.setTransferable(this);
 289		this.mStatus = Transferable.STATUS_UPLOADING;
 290		this.initiator = this.account.getJid();
 291		this.responder = this.message.getCounterpart();
 292		this.sessionId = this.mJingleConnectionManager.nextRandomId();
 293		this.transportId = this.mJingleConnectionManager.nextRandomId();
 294		if (this.candidates.size() > 0) {
 295			this.sendInitRequest();
 296		} else {
 297			this.mJingleConnectionManager.getPrimaryCandidate(account,
 298					new OnPrimaryCandidateFound() {
 299
 300						@Override
 301						public void onPrimaryCandidateFound(boolean success,
 302															final JingleCandidate candidate) {
 303							if (success) {
 304								final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
 305										JingleConnection.this, candidate);
 306								connections.put(candidate.getCid(),
 307										socksConnection);
 308								socksConnection
 309										.connect(new OnTransportConnected() {
 310
 311											@Override
 312											public void failed() {
 313												Log.d(Config.LOGTAG,
 314														"connection to our own primary candidete failed");
 315												sendInitRequest();
 316											}
 317
 318											@Override
 319											public void established() {
 320												Log.d(Config.LOGTAG,
 321														"successfully connected to our own primary candidate");
 322												mergeCandidate(candidate);
 323												sendInitRequest();
 324											}
 325										});
 326								mergeCandidate(candidate);
 327							} else {
 328								Log.d(Config.LOGTAG, "no primary candidate of our own was found");
 329								sendInitRequest();
 330							}
 331						}
 332					});
 333		}
 334
 335	}
 336
 337	private void upgradeNamespace() {
 338		Jid jid = this.message.getCounterpart();
 339		String resource = jid != null ?jid.getResource() : null;
 340		if (resource != null) {
 341			Presence presence = this.account.getRoster().getContact(jid).getPresences().getPresences().get(resource);
 342			ServiceDiscoveryResult result = presence != null ? presence.getServiceDiscoveryResult() : null;
 343			if (result != null) {
 344				List<String> features = result.getFeatures();
 345				if (features.contains(Content.Version.FT_5.getNamespace())) {
 346					this.ftVersion = Content.Version.FT_5;
 347				} else if (features.contains(Content.Version.FT_4.getNamespace())) {
 348					this.ftVersion = Content.Version.FT_4;
 349				}
 350			}
 351		}
 352	}
 353
 354	public void init(Account account, JinglePacket packet) {
 355		this.mJingleStatus = JINGLE_STATUS_INITIATED;
 356		Conversation conversation = this.mXmppConnectionService
 357				.findOrCreateConversation(account,
 358						packet.getFrom().asBareJid(), false, false);
 359		this.message = new Message(conversation, "", Message.ENCRYPTION_NONE);
 360		this.message.setStatus(Message.STATUS_RECEIVED);
 361		this.mStatus = Transferable.STATUS_OFFER;
 362		this.message.setTransferable(this);
 363        final Jid from = packet.getFrom();
 364		this.message.setCounterpart(from);
 365		this.account = account;
 366		this.initiator = packet.getFrom();
 367		this.responder = this.account.getJid();
 368		this.sessionId = packet.getSessionId();
 369		Content content = packet.getJingleContent();
 370		this.contentCreator = content.getAttribute("creator");
 371		this.contentName = content.getAttribute("name");
 372		this.transportId = content.getTransportId();
 373		this.mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren()));
 374		this.ftVersion = content.getVersion();
 375		if (ftVersion == null) {
 376			this.sendCancel();
 377			this.fail();
 378			return;
 379		}
 380		this.fileOffer = content.getFileOffer(this.ftVersion);
 381
 382		mXmppConnectionService.sendIqPacket(account,packet.generateResponse(IqPacket.TYPE.RESULT),null);
 383
 384		if (fileOffer != null) {
 385			Element encrypted = fileOffer.findChild("encrypted", AxolotlService.PEP_PREFIX);
 386			if (encrypted != null) {
 387				this.mXmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, packet.getFrom().asBareJid());
 388			}
 389			Element fileSize = fileOffer.findChild("size");
 390			Element fileNameElement = fileOffer.findChild("name");
 391			if (fileNameElement != null) {
 392				String[] filename = fileNameElement.getContent()
 393						.toLowerCase(Locale.US).toLowerCase().split("\\.");
 394				String extension = filename[filename.length - 1];
 395				if (VALID_IMAGE_EXTENSIONS.contains(extension)) {
 396					message.setType(Message.TYPE_IMAGE);
 397					message.setRelativeFilePath(message.getUuid()+"."+extension);
 398				} else if (VALID_CRYPTO_EXTENSIONS.contains(
 399						filename[filename.length - 1])) {
 400					if (filename.length == 3) {
 401						extension = filename[filename.length - 2];
 402						if (VALID_IMAGE_EXTENSIONS.contains(extension)) {
 403							message.setType(Message.TYPE_IMAGE);
 404							message.setRelativeFilePath(message.getUuid()+"."+extension);
 405						} else {
 406							message.setType(Message.TYPE_FILE);
 407						}
 408						message.setEncryption(Message.ENCRYPTION_PGP);
 409					}
 410				} else {
 411					message.setType(Message.TYPE_FILE);
 412				}
 413				if (message.getType() == Message.TYPE_FILE) {
 414					String suffix = "";
 415					if (!fileNameElement.getContent().isEmpty()) {
 416						String parts[] = fileNameElement.getContent().split("/");
 417						suffix = parts[parts.length - 1];
 418						if (message.getEncryption() == Message.ENCRYPTION_PGP && (suffix.endsWith(".pgp") || suffix.endsWith(".gpg"))) {
 419							suffix = suffix.substring(0,suffix.length() - 4);
 420						}
 421					}
 422					message.setRelativeFilePath(message.getUuid()+"_"+suffix);
 423				}
 424				long size = Long.parseLong(fileSize.getContent());
 425				message.setBody(Long.toString(size));
 426				conversation.add(message);
 427				mJingleConnectionManager.updateConversationUi(true);
 428				if (mJingleConnectionManager.hasStoragePermission()
 429						&& size < this.mJingleConnectionManager.getAutoAcceptFileSize()
 430						&& mXmppConnectionService.isDataSaverDisabled()) {
 431					Log.d(Config.LOGTAG, "auto accepting file from "+ packet.getFrom());
 432					this.acceptedAutomatically = true;
 433					this.sendAccept();
 434				} else {
 435					message.markUnread();
 436					Log.d(Config.LOGTAG,
 437							"not auto accepting new file offer with size: "
 438									+ size
 439									+ " allowed size:"
 440									+ this.mJingleConnectionManager
 441											.getAutoAcceptFileSize());
 442					this.mXmppConnectionService.getNotificationService().push(message);
 443				}
 444				this.file = this.mXmppConnectionService.getFileBackend().getFile(message, false);
 445				if (mXmppAxolotlMessage != null) {
 446					XmppAxolotlMessage.XmppAxolotlKeyTransportMessage transportMessage = account.getAxolotlService().processReceivingKeyTransportMessage(mXmppAxolotlMessage, false);
 447					if (transportMessage != null) {
 448						message.setEncryption(Message.ENCRYPTION_AXOLOTL);
 449						this.file.setKey(transportMessage.getKey());
 450						this.file.setIv(transportMessage.getIv());
 451						message.setFingerprint(transportMessage.getFingerprint());
 452					} else {
 453						Log.d(Config.LOGTAG,"could not process KeyTransportMessage");
 454					}
 455				}
 456				this.file.setExpectedSize(size);
 457				message.resetFileParams();
 458				Log.d(Config.LOGTAG, "receiving file: expecting size of " + this.file.getExpectedSize());
 459			} else {
 460				this.sendCancel();
 461				this.fail();
 462			}
 463		} else {
 464			this.sendCancel();
 465			this.fail();
 466		}
 467	}
 468
 469	private void sendInitRequest() {
 470		JinglePacket packet = this.bootstrapPacket("session-initiate");
 471		Content content = new Content(this.contentCreator, this.contentName);
 472		if (message.isFileOrImage()) {
 473			content.setTransportId(this.transportId);
 474			this.file = this.mXmppConnectionService.getFileBackend().getFile(message, false);
 475			if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
 476				this.file.setKey(mXmppAxolotlMessage.getInnerKey());
 477				this.file.setIv(mXmppAxolotlMessage.getIV());
 478				this.file.setExpectedSize(file.getSize() + 16);
 479				content.setFileOffer(this.file, false, this.ftVersion).addChild(mXmppAxolotlMessage.toElement());
 480			} else {
 481				this.file.setExpectedSize(file.getSize());
 482				content.setFileOffer(this.file, false, this.ftVersion);
 483			}
 484			message.resetFileParams();
 485			try {
 486				this.mFileInputStream = new FileInputStream(file);
 487			} catch (FileNotFoundException e) {
 488				abort();
 489				return;
 490			}
 491			content.setTransportId(this.transportId);
 492			content.socks5transport().setChildren(getCandidatesAsElements());
 493			packet.setContent(content);
 494			this.sendJinglePacket(packet,new OnIqPacketReceived() {
 495
 496				@Override
 497				public void onIqPacketReceived(Account account, IqPacket packet) {
 498					if (packet.getType() == IqPacket.TYPE.RESULT) {
 499						Log.d(Config.LOGTAG,account.getJid().asBareJid()+": other party received offer");
 500						if (mJingleStatus == JINGLE_STATUS_OFFERED) {
 501							mJingleStatus = JINGLE_STATUS_INITIATED;
 502							mXmppConnectionService.markMessage(message, Message.STATUS_OFFERED);
 503						} else {
 504							Log.d(Config.LOGTAG,"received ack for offer when status was "+mJingleStatus);
 505						}
 506					} else {
 507						fail(IqParser.extractErrorMessage(packet));
 508					}
 509				}
 510			});
 511
 512		}
 513	}
 514
 515	private void sendHash() {
 516		JinglePacket packet = this.bootstrapPacket("session-info");
 517		packet.addChecksum(file.getSha1Sum(),ftVersion.getNamespace());
 518		this.sendJinglePacket(packet);
 519	}
 520
 521	private List<Element> getCandidatesAsElements() {
 522		List<Element> elements = new ArrayList<>();
 523		for (JingleCandidate c : this.candidates) {
 524			if (c.isOurs()) {
 525				elements.add(c.toElement());
 526			}
 527		}
 528		return elements;
 529	}
 530
 531	private void sendAccept() {
 532		mJingleStatus = JINGLE_STATUS_ACCEPTED;
 533		this.mStatus = Transferable.STATUS_DOWNLOADING;
 534		this.mJingleConnectionManager.updateConversationUi(true);
 535		this.mJingleConnectionManager.getPrimaryCandidate(this.account, new OnPrimaryCandidateFound() {
 536			@Override
 537			public void onPrimaryCandidateFound(boolean success, final JingleCandidate candidate) {
 538				final JinglePacket packet = bootstrapPacket("session-accept");
 539				final Content content = new Content(contentCreator,contentName);
 540				content.setFileOffer(fileOffer, ftVersion);
 541				content.setTransportId(transportId);
 542				if (success && candidate != null && !equalCandidateExists(candidate)) {
 543					final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
 544							JingleConnection.this,
 545							candidate);
 546					connections.put(candidate.getCid(), socksConnection);
 547					socksConnection.connect(new OnTransportConnected() {
 548
 549						@Override
 550						public void failed() {
 551							Log.d(Config.LOGTAG,"connection to our own primary candidate failed");
 552							content.socks5transport().setChildren(getCandidatesAsElements());
 553							packet.setContent(content);
 554							sendJinglePacket(packet);
 555							connectNextCandidate();
 556						}
 557
 558						@Override
 559						public void established() {
 560							Log.d(Config.LOGTAG, "connected to primary candidate");
 561							mergeCandidate(candidate);
 562							content.socks5transport().setChildren(getCandidatesAsElements());
 563							packet.setContent(content);
 564							sendJinglePacket(packet);
 565							connectNextCandidate();
 566						}
 567					});
 568				} else {
 569					Log.d(Config.LOGTAG,"did not find a primary candidate for ourself");
 570					content.socks5transport().setChildren(getCandidatesAsElements());
 571					packet.setContent(content);
 572					sendJinglePacket(packet);
 573					connectNextCandidate();
 574				}
 575			}
 576		});
 577	}
 578
 579	private JinglePacket bootstrapPacket(String action) {
 580		JinglePacket packet = new JinglePacket();
 581		packet.setAction(action);
 582		packet.setFrom(account.getJid());
 583		packet.setTo(this.message.getCounterpart());
 584		packet.setSessionId(this.sessionId);
 585		packet.setInitiator(this.initiator);
 586		return packet;
 587	}
 588
 589	private void sendJinglePacket(JinglePacket packet) {
 590		mXmppConnectionService.sendIqPacket(account,packet,responseListener);
 591	}
 592
 593	private void sendJinglePacket(JinglePacket packet, OnIqPacketReceived callback) {
 594		mXmppConnectionService.sendIqPacket(account,packet,callback);
 595	}
 596
 597	private boolean receiveAccept(JinglePacket packet) {
 598		Content content = packet.getJingleContent();
 599		mergeCandidates(JingleCandidate.parse(content.socks5transport()
 600				.getChildren()));
 601		this.mJingleStatus = JINGLE_STATUS_ACCEPTED;
 602		mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
 603		this.connectNextCandidate();
 604		return true;
 605	}
 606
 607	private boolean receiveTransportInfo(JinglePacket packet) {
 608		Content content = packet.getJingleContent();
 609		if (content.hasSocks5Transport()) {
 610			if (content.socks5transport().hasChild("activated")) {
 611				if ((this.transport != null) && (this.transport instanceof JingleSocks5Transport)) {
 612					onProxyActivated.success();
 613				} else {
 614					String cid = content.socks5transport().findChild("activated").getAttribute("cid");
 615					Log.d(Config.LOGTAG, "received proxy activated (" + cid
 616							+ ")prior to choosing our own transport");
 617					JingleSocks5Transport connection = this.connections.get(cid);
 618					if (connection != null) {
 619						connection.setActivated(true);
 620					} else {
 621						Log.d(Config.LOGTAG, "activated connection not found");
 622						this.sendCancel();
 623						this.fail();
 624					}
 625				}
 626				return true;
 627			} else if (content.socks5transport().hasChild("proxy-error")) {
 628				onProxyActivated.failed();
 629				return true;
 630			} else if (content.socks5transport().hasChild("candidate-error")) {
 631				Log.d(Config.LOGTAG, "received candidate error");
 632				this.receivedCandidate = true;
 633				if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) {
 634					this.connect();
 635				}
 636				return true;
 637			} else if (content.socks5transport().hasChild("candidate-used")) {
 638				String cid = content.socks5transport()
 639						.findChild("candidate-used").getAttribute("cid");
 640				if (cid != null) {
 641					Log.d(Config.LOGTAG, "candidate used by counterpart:" + cid);
 642					JingleCandidate candidate = getCandidate(cid);
 643					if (candidate == null) {
 644						Log.d(Config.LOGTAG,"could not find candidate with cid="+cid);
 645						return false;
 646					}
 647					candidate.flagAsUsedByCounterpart();
 648					this.receivedCandidate = true;
 649					if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) {
 650						this.connect();
 651					} else {
 652						Log.d(Config.LOGTAG, "ignoring because file is already in transmission or we haven't sent our candidate yet status="+mJingleStatus+" sentCandidate="+Boolean.toString(sentCandidate));
 653					}
 654					return true;
 655				} else {
 656					return false;
 657				}
 658			} else {
 659				return false;
 660			}
 661		} else {
 662			return true;
 663		}
 664	}
 665
 666	private void connect() {
 667		final JingleSocks5Transport connection = chooseConnection();
 668		this.transport = connection;
 669		if (connection == null) {
 670			Log.d(Config.LOGTAG, "could not find suitable candidate");
 671			this.disconnectSocks5Connections();
 672			if (initiating()) {
 673				this.sendFallbackToIbb();
 674			}
 675		} else {
 676			this.mJingleStatus = JINGLE_STATUS_TRANSMITTING;
 677			if (connection.needsActivation()) {
 678				if (connection.getCandidate().isOurs()) {
 679					final String sid;
 680					if (ftVersion == Content.Version.FT_3) {
 681						Log.d(Config.LOGTAG,account.getJid().asBareJid()+": use session ID instead of transport ID to activate proxy");
 682						sid = getSessionId();
 683					} else {
 684						sid = getTransportId();
 685					}
 686					Log.d(Config.LOGTAG, "candidate "
 687							+ connection.getCandidate().getCid()
 688							+ " was our proxy. going to activate");
 689					IqPacket activation = new IqPacket(IqPacket.TYPE.SET);
 690					activation.setTo(connection.getCandidate().getJid());
 691					activation.query("http://jabber.org/protocol/bytestreams")
 692							.setAttribute("sid", sid);
 693					activation.query().addChild("activate")
 694							.setContent(this.getCounterPart().toString());
 695					mXmppConnectionService.sendIqPacket(account,activation,
 696							new OnIqPacketReceived() {
 697
 698								@Override
 699								public void onIqPacketReceived(Account account,
 700										IqPacket packet) {
 701									if (packet.getType() != IqPacket.TYPE.RESULT) {
 702										onProxyActivated.failed();
 703									} else {
 704										onProxyActivated.success();
 705										sendProxyActivated(connection.getCandidate().getCid());
 706									}
 707								}
 708							});
 709				} else {
 710					Log.d(Config.LOGTAG,
 711							"candidate "
 712									+ connection.getCandidate().getCid()
 713									+ " was a proxy. waiting for other party to activate");
 714				}
 715			} else {
 716				if (initiating()) {
 717					Log.d(Config.LOGTAG, "we were initiating. sending file");
 718					connection.send(file, onFileTransmissionStatusChanged);
 719				} else {
 720					Log.d(Config.LOGTAG, "we were responding. receiving file");
 721					connection.receive(file, onFileTransmissionStatusChanged);
 722				}
 723			}
 724		}
 725	}
 726
 727	private JingleSocks5Transport chooseConnection() {
 728		JingleSocks5Transport connection = null;
 729		for (Entry<String, JingleSocks5Transport> cursor : connections
 730				.entrySet()) {
 731			JingleSocks5Transport currentConnection = cursor.getValue();
 732			// Log.d(Config.LOGTAG,"comparing candidate: "+currentConnection.getCandidate().toString());
 733			if (currentConnection.isEstablished()
 734					&& (currentConnection.getCandidate().isUsedByCounterpart() || (!currentConnection
 735							.getCandidate().isOurs()))) {
 736				// Log.d(Config.LOGTAG,"is usable");
 737				if (connection == null) {
 738					connection = currentConnection;
 739				} else {
 740					if (connection.getCandidate().getPriority() < currentConnection
 741							.getCandidate().getPriority()) {
 742						connection = currentConnection;
 743					} else if (connection.getCandidate().getPriority() == currentConnection
 744							.getCandidate().getPriority()) {
 745						// Log.d(Config.LOGTAG,"found two candidates with same priority");
 746						if (initiating()) {
 747							if (currentConnection.getCandidate().isOurs()) {
 748								connection = currentConnection;
 749							}
 750						} else {
 751							if (!currentConnection.getCandidate().isOurs()) {
 752								connection = currentConnection;
 753							}
 754						}
 755					}
 756				}
 757			}
 758		}
 759		return connection;
 760	}
 761
 762	private void sendSuccess() {
 763		JinglePacket packet = bootstrapPacket("session-terminate");
 764		Reason reason = new Reason();
 765		reason.addChild("success");
 766		packet.setReason(reason);
 767		this.sendJinglePacket(packet);
 768		this.disconnectSocks5Connections();
 769		this.mJingleStatus = JINGLE_STATUS_FINISHED;
 770		this.message.setStatus(Message.STATUS_RECEIVED);
 771		this.message.setTransferable(null);
 772		this.mXmppConnectionService.updateMessage(message, false);
 773		this.mJingleConnectionManager.finishConnection(this);
 774	}
 775
 776	private void sendFallbackToIbb() {
 777		Log.d(Config.LOGTAG, account.getJid().asBareJid()+": sending fallback to ibb");
 778		JinglePacket packet = this.bootstrapPacket("transport-replace");
 779		Content content = new Content(this.contentCreator, this.contentName);
 780		this.transportId = this.mJingleConnectionManager.nextRandomId();
 781		content.setTransportId(this.transportId);
 782		content.ibbTransport().setAttribute("block-size",
 783				Integer.toString(this.ibbBlockSize));
 784		packet.setContent(content);
 785		this.sendJinglePacket(packet);
 786	}
 787
 788	OnTransportConnected onIbbTransportConnected = new OnTransportConnected() {
 789		@Override
 790		public void failed() {
 791			Log.d(Config.LOGTAG, "ibb open failed");
 792		}
 793
 794		@Override
 795		public void established() {
 796			JingleConnection.this.transport.send(file, onFileTransmissionStatusChanged);
 797		}
 798	};
 799
 800	private boolean receiveFallbackToIbb(JinglePacket packet) {
 801		Log.d(Config.LOGTAG, "receiving fallack to ibb");
 802		String receivedBlockSize = packet.getJingleContent().ibbTransport()
 803				.getAttribute("block-size");
 804		if (receivedBlockSize != null) {
 805			int bs = Integer.parseInt(receivedBlockSize);
 806			if (bs > this.ibbBlockSize) {
 807				this.ibbBlockSize = bs;
 808			}
 809		}
 810		this.transportId = packet.getJingleContent().getTransportId();
 811		this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
 812
 813		JinglePacket answer = bootstrapPacket("transport-accept");
 814		Content content = new Content("initiator", "a-file-offer");
 815		content.setTransportId(this.transportId);
 816		content.ibbTransport().setAttribute("block-size",this.ibbBlockSize);
 817		answer.setContent(content);
 818
 819
 820		if (initiating()) {
 821			this.sendJinglePacket(answer, new OnIqPacketReceived() {
 822				@Override
 823				public void onIqPacketReceived(Account account, IqPacket packet) {
 824					if (packet.getType() == IqPacket.TYPE.RESULT) {
 825						Log.d(Config.LOGTAG, account.getJid().asBareJid() + " recipient ACKed our transport-accept. creating ibb");
 826						transport.connect(onIbbTransportConnected);
 827					}
 828				}
 829			});
 830		} else {
 831			this.transport.receive(file, onFileTransmissionStatusChanged);
 832			this.sendJinglePacket(answer);
 833		}
 834		return true;
 835	}
 836
 837	private boolean receiveTransportAccept(JinglePacket packet) {
 838		if (packet.getJingleContent().hasIbbTransport()) {
 839			String receivedBlockSize = packet.getJingleContent().ibbTransport()
 840					.getAttribute("block-size");
 841			if (receivedBlockSize != null) {
 842				int bs = Integer.parseInt(receivedBlockSize);
 843				if (bs > this.ibbBlockSize) {
 844					this.ibbBlockSize = bs;
 845				}
 846			}
 847			this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
 848
 849			//might be receive instead if we are not initiating
 850			if (initiating()) {
 851				this.transport.connect(onIbbTransportConnected);
 852			} else {
 853				this.transport.receive(file, onFileTransmissionStatusChanged);
 854			}
 855			return true;
 856		} else {
 857			return false;
 858		}
 859	}
 860
 861	private void receiveSuccess() {
 862		if (initiating()) {
 863			this.mJingleStatus = JINGLE_STATUS_FINISHED;
 864			this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND_RECEIVED);
 865			this.disconnectSocks5Connections();
 866			if (this.transport instanceof JingleInbandTransport) {
 867				this.transport.disconnect();
 868			}
 869			this.message.setTransferable(null);
 870			this.mJingleConnectionManager.finishConnection(this);
 871		} else {
 872			Log.d(Config.LOGTAG,account.getJid().asBareJid()+": received session-terminate/success while responding");
 873		}
 874	}
 875	@Override
 876	public void cancel() {
 877		this.cancelled = true;
 878		abort();
 879	}
 880
 881	public void abort() {
 882		this.disconnectSocks5Connections();
 883		if (this.transport instanceof JingleInbandTransport) {
 884			this.transport.disconnect();
 885		}
 886		this.sendCancel();
 887		this.mJingleConnectionManager.finishConnection(this);
 888		if (responding()) {
 889			this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_FAILED));
 890			if (this.file!=null) {
 891				file.delete();
 892			}
 893			this.mJingleConnectionManager.updateConversationUi(true);
 894		} else {
 895			this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : null);
 896			this.message.setTransferable(null);
 897		}
 898	}
 899
 900	private void fail() {
 901		fail(null);
 902	}
 903
 904	private void fail(String errorMessage) {
 905		this.mJingleStatus = JINGLE_STATUS_FAILED;
 906		this.disconnectSocks5Connections();
 907		if (this.transport instanceof JingleInbandTransport) {
 908			this.transport.disconnect();
 909		}
 910		FileBackend.close(mFileInputStream);
 911		FileBackend.close(mFileOutputStream);
 912		if (this.message != null) {
 913			if (responding()) {
 914				this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_FAILED));
 915				if (this.file!=null) {
 916					file.delete();
 917				}
 918				this.mJingleConnectionManager.updateConversationUi(true);
 919			} else {
 920				this.mXmppConnectionService.markMessage(this.message,
 921						Message.STATUS_SEND_FAILED,
 922						cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage);
 923				this.message.setTransferable(null);
 924			}
 925		}
 926		this.mJingleConnectionManager.finishConnection(this);
 927	}
 928
 929	private void sendCancel() {
 930		JinglePacket packet = bootstrapPacket("session-terminate");
 931		Reason reason = new Reason();
 932		reason.addChild("cancel");
 933		packet.setReason(reason);
 934		this.sendJinglePacket(packet);
 935	}
 936
 937	private void connectNextCandidate() {
 938		for (JingleCandidate candidate : this.candidates) {
 939			if ((!connections.containsKey(candidate.getCid()) && (!candidate
 940					.isOurs()))) {
 941				this.connectWithCandidate(candidate);
 942				return;
 943			}
 944		}
 945		this.sendCandidateError();
 946	}
 947
 948	private void connectWithCandidate(final JingleCandidate candidate) {
 949		final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
 950				this, candidate);
 951		connections.put(candidate.getCid(), socksConnection);
 952		socksConnection.connect(new OnTransportConnected() {
 953
 954			@Override
 955			public void failed() {
 956				Log.d(Config.LOGTAG,
 957						"connection failed with " + candidate.getHost() + ":"
 958								+ candidate.getPort());
 959				connectNextCandidate();
 960			}
 961
 962			@Override
 963			public void established() {
 964				Log.d(Config.LOGTAG,
 965						"established connection with " + candidate.getHost()
 966								+ ":" + candidate.getPort());
 967				sendCandidateUsed(candidate.getCid());
 968			}
 969		});
 970	}
 971
 972	private void disconnectSocks5Connections() {
 973		Iterator<Entry<String, JingleSocks5Transport>> it = this.connections
 974				.entrySet().iterator();
 975		while (it.hasNext()) {
 976			Entry<String, JingleSocks5Transport> pairs = it.next();
 977			pairs.getValue().disconnect();
 978			it.remove();
 979		}
 980	}
 981
 982	private void sendProxyActivated(String cid) {
 983		JinglePacket packet = bootstrapPacket("transport-info");
 984		Content content = new Content(this.contentCreator, this.contentName);
 985		content.setTransportId(this.transportId);
 986		content.socks5transport().addChild("activated")
 987				.setAttribute("cid", cid);
 988		packet.setContent(content);
 989		this.sendJinglePacket(packet);
 990	}
 991
 992	private void sendCandidateUsed(final String cid) {
 993		JinglePacket packet = bootstrapPacket("transport-info");
 994		Content content = new Content(this.contentCreator, this.contentName);
 995		content.setTransportId(this.transportId);
 996		content.socks5transport().addChild("candidate-used").setAttribute("cid", cid);
 997		packet.setContent(content);
 998		this.sentCandidate = true;
 999		if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
1000			connect();
1001		}
1002		this.sendJinglePacket(packet);
1003	}
1004
1005	private void sendCandidateError() {
1006		JinglePacket packet = bootstrapPacket("transport-info");
1007		Content content = new Content(this.contentCreator, this.contentName);
1008		content.setTransportId(this.transportId);
1009		content.socks5transport().addChild("candidate-error");
1010		packet.setContent(content);
1011		this.sentCandidate = true;
1012		if (receivedCandidate && mJingleStatus == JINGLE_STATUS_ACCEPTED) {
1013			connect();
1014		}
1015		this.sendJinglePacket(packet);
1016	}
1017
1018	public int getJingleStatus() {
1019		return this.mJingleStatus;
1020	}
1021
1022	private boolean equalCandidateExists(JingleCandidate candidate) {
1023		for (JingleCandidate c : this.candidates) {
1024			if (c.equalValues(candidate)) {
1025				return true;
1026			}
1027		}
1028		return false;
1029	}
1030
1031	private void mergeCandidate(JingleCandidate candidate) {
1032		for (JingleCandidate c : this.candidates) {
1033			if (c.equals(candidate)) {
1034				return;
1035			}
1036		}
1037		this.candidates.add(candidate);
1038	}
1039
1040	private void mergeCandidates(List<JingleCandidate> candidates) {
1041		for (JingleCandidate c : candidates) {
1042			mergeCandidate(c);
1043		}
1044	}
1045
1046	private JingleCandidate getCandidate(String cid) {
1047		for (JingleCandidate c : this.candidates) {
1048			if (c.getCid().equals(cid)) {
1049				return c;
1050			}
1051		}
1052		return null;
1053	}
1054
1055	public void updateProgress(int i) {
1056		this.mProgress = i;
1057		mJingleConnectionManager.updateConversationUi(false);
1058	}
1059
1060	public String getTransportId() {
1061		return this.transportId;
1062	}
1063
1064	public Content.Version getFtVersion() {
1065		return this.ftVersion;
1066	}
1067
1068	interface OnProxyActivated {
1069		void success();
1070
1071		void failed();
1072	}
1073
1074	public boolean hasTransportId(String sid) {
1075		return sid.equals(this.transportId);
1076	}
1077
1078	public JingleTransport getTransport() {
1079		return this.transport;
1080	}
1081
1082	public boolean start() {
1083		if (account.getStatus() == Account.State.ONLINE) {
1084			if (mJingleStatus == JINGLE_STATUS_INITIATED) {
1085				new Thread(new Runnable() {
1086
1087					@Override
1088					public void run() {
1089						sendAccept();
1090					}
1091				}).start();
1092			}
1093			return true;
1094		} else {
1095			return false;
1096		}
1097	}
1098
1099	@Override
1100	public int getStatus() {
1101		return this.mStatus;
1102	}
1103
1104	@Override
1105	public long getFileSize() {
1106		if (this.file != null) {
1107			return this.file.getExpectedSize();
1108		} else {
1109			return 0;
1110		}
1111	}
1112
1113	@Override
1114	public int getProgress() {
1115		return this.mProgress;
1116	}
1117
1118	public AbstractConnectionManager getConnectionManager() {
1119		return this.mJingleConnectionManager;
1120	}
1121}