JingleConnection.java

  1package eu.siacs.conversations.xmpp.jingle;
  2
  3import java.util.ArrayList;
  4import java.util.HashMap;
  5import java.util.Iterator;
  6import java.util.List;
  7import java.util.Map.Entry;
  8
  9import android.util.Log;
 10
 11import eu.siacs.conversations.entities.Account;
 12import eu.siacs.conversations.entities.Conversation;
 13import eu.siacs.conversations.entities.Message;
 14import eu.siacs.conversations.services.XmppConnectionService;
 15import eu.siacs.conversations.xml.Element;
 16import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 17import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
 18import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
 19import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
 20import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 21
 22public class JingleConnection {
 23
 24	private JingleConnectionManager mJingleConnectionManager;
 25	private XmppConnectionService mXmppConnectionService;
 26	
 27	public static final int STATUS_INITIATED = 0;
 28	public static final int STATUS_ACCEPTED = 1;
 29	public static final int STATUS_TERMINATED = 2;
 30	public static final int STATUS_CANCELED = 3;
 31	public static final int STATUS_FINISHED = 4;
 32	public static final int STATUS_TRANSMITTING = 5;
 33	public static final int STATUS_FAILED = 99;
 34	
 35	private int ibbBlockSize = 4096;
 36	
 37	private int status = -1;
 38	private Message message;
 39	private String sessionId;
 40	private Account account;
 41	private String initiator;
 42	private String responder;
 43	private List<JingleCandidate> candidates = new ArrayList<JingleCandidate>();
 44	private HashMap<String, JingleSocks5Transport> connections = new HashMap<String, JingleSocks5Transport>();
 45	
 46	private String transportId;
 47	private Element fileOffer;
 48	private JingleFile file = null;
 49	
 50	private boolean receivedCandidate = false;
 51	private boolean sentCandidate = false;
 52	
 53	private boolean acceptedAutomatically = false;
 54	
 55	private JingleTransport transport = null;
 56	
 57	private OnIqPacketReceived responseListener = new OnIqPacketReceived() {
 58		
 59		@Override
 60		public void onIqPacketReceived(Account account, IqPacket packet) {
 61			if (packet.getType() == IqPacket.TYPE_ERROR) {
 62				mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
 63				status = STATUS_FAILED;
 64			}
 65		}
 66	};
 67	
 68	final OnFileTransmitted onFileTransmitted = new OnFileTransmitted() {
 69		
 70		@Override
 71		public void onFileTransmitted(JingleFile file) {
 72			if (responder.equals(account.getFullJid())) {
 73				sendSuccess();
 74				if (acceptedAutomatically) {
 75					message.markUnread();
 76				}
 77				mXmppConnectionService.markMessage(message, Message.STATUS_RECIEVED);
 78			}
 79			mXmppConnectionService.databaseBackend.createMessage(message);
 80			Log.d("xmppService","sucessfully transmitted file. sha1:"+file.getSha1Sum());
 81		}
 82	};
 83	
 84	private OnProxyActivated onProxyActivated = new OnProxyActivated() {
 85		
 86		@Override
 87		public void success() {
 88			if (initiator.equals(account.getFullJid())) {
 89				Log.d("xmppService","we were initiating. sending file");
 90				transport.send(file,onFileTransmitted);
 91			} else {
 92				transport.receive(file,onFileTransmitted);
 93				Log.d("xmppService","we were responding. receiving file");
 94			}
 95		}
 96		
 97		@Override
 98		public void failed() {
 99			Log.d("xmppService","proxy activation failed");
100		}
101	};
102	
103	public JingleConnection(JingleConnectionManager mJingleConnectionManager) {
104		this.mJingleConnectionManager = mJingleConnectionManager;
105		this.mXmppConnectionService = mJingleConnectionManager.getXmppConnectionService();
106	}
107	
108	public String getSessionId() {
109		return this.sessionId;
110	}
111	
112	public String getAccountJid() {
113		return this.account.getFullJid();
114	}
115	
116	public String getCounterPart() {
117		return this.message.getCounterpart();
118	}
119	
120	public void deliverPacket(JinglePacket packet) {
121		
122		if (packet.isAction("session-terminate")) {
123			Reason reason = packet.getReason();
124			if (reason!=null) {
125				if (reason.hasChild("cancel")) {
126					this.cancel();
127				} else if (reason.hasChild("success")) {
128					this.finish();
129				}
130			} else {
131				Log.d("xmppService","remote terminated for no reason");
132				this.cancel();
133			}
134			} else if (packet.isAction("session-accept")) {
135			receiveAccept(packet);
136		} else if (packet.isAction("transport-info")) {
137			receiveTransportInfo(packet);
138		} else if (packet.isAction("transport-replace")) {
139			if (packet.getJingleContent().hasIbbTransport()) {
140				this.receiveFallbackToIbb(packet);
141			} else {
142				Log.d("xmppService","trying to fallback to something unknown"+packet.toString());
143			}
144		} else if (packet.isAction("transport-accept")) {
145			this.receiveTransportAccept(packet);
146		} else {
147			Log.d("xmppService","packet arrived in connection. action was "+packet.getAction());
148		}
149	}
150	
151	public void init(Message message) {
152		this.message = message;
153		this.account = message.getConversation().getAccount();
154		this.initiator = this.account.getFullJid();
155		this.responder = this.message.getCounterpart();
156		this.sessionId = this.mJingleConnectionManager.nextRandomId();
157		if (this.candidates.size() > 0) {
158			this.sendInitRequest();
159		} else {
160			this.mJingleConnectionManager.getPrimaryCandidate(account, new OnPrimaryCandidateFound() {
161				
162				@Override
163				public void onPrimaryCandidateFound(boolean success, final JingleCandidate candidate) {
164					if (success) {
165						final JingleSocks5Transport socksConnection = new JingleSocks5Transport(JingleConnection.this, candidate);
166						connections.put(candidate.getCid(), socksConnection);
167						socksConnection.connect(new OnTransportConnected() {
168							
169							@Override
170							public void failed() {
171								Log.d("xmppService","connection to our own primary candidete failed");
172								sendInitRequest();
173							}
174							
175							@Override
176							public void established() {
177								Log.d("xmppService","succesfully connected to our own primary candidate");
178								mergeCandidate(candidate);
179								sendInitRequest();
180							}
181						});
182						mergeCandidate(candidate);
183					} else {
184						Log.d("xmppService","no primary candidate of our own was found");
185						sendInitRequest();
186					}
187				}
188			});
189		}
190		
191	}
192	
193	public void init(Account account, JinglePacket packet) {
194		this.status = STATUS_INITIATED;
195		Conversation conversation = this.mXmppConnectionService.findOrCreateConversation(account, packet.getFrom().split("/")[0], false);
196		this.message = new Message(conversation, "receiving image file", Message.ENCRYPTION_NONE);
197		this.message.setType(Message.TYPE_IMAGE);
198		this.message.setStatus(Message.STATUS_RECEIVED_OFFER);
199		this.message.setJingleConnection(this);
200		String[] fromParts = packet.getFrom().split("/");
201		this.message.setPresence(fromParts[1]);
202		this.account = account;
203		this.initiator = packet.getFrom();
204		this.responder = this.account.getFullJid();
205		this.sessionId = packet.getSessionId();
206		Content content = packet.getJingleContent();
207		this.transportId = content.getTransportId();
208		this.mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren()));
209		this.fileOffer = packet.getJingleContent().getFileOffer();
210		if (fileOffer!=null) {
211			this.file = this.mXmppConnectionService.getFileBackend().getJingleFile(message);
212			Element fileSize = fileOffer.findChild("size");
213			Element fileName = fileOffer.findChild("name");
214			this.file.setExpectedSize(Long.parseLong(fileSize.getContent()));
215			conversation.getMessages().add(message);
216			if (this.file.getExpectedSize()<=this.mJingleConnectionManager.getAutoAcceptFileSize()) {
217				Log.d("xmppService","auto accepting file from "+packet.getFrom());
218				this.acceptedAutomatically = true;
219				this.sendAccept();
220			} else {
221				message.markUnread();
222				Log.d("xmppService","not auto accepting new file offer with size: "+this.file.getExpectedSize()+" allowed size:"+this.mJingleConnectionManager.getAutoAcceptFileSize());
223				if (this.mXmppConnectionService.convChangedListener!=null) {
224					this.mXmppConnectionService.convChangedListener.onConversationListChanged();
225				}
226			}
227		} else {
228			Log.d("xmppService","no file offer was attached. aborting");
229		}
230	}
231	
232	private void sendInitRequest() {
233		JinglePacket packet = this.bootstrapPacket("session-initiate");
234		Content content = new Content();
235		if (message.getType() == Message.TYPE_IMAGE) {
236			content.setAttribute("creator", "initiator");
237			content.setAttribute("name", "a-file-offer");
238			content.setTransportId(this.transportId);
239			this.file = this.mXmppConnectionService.getFileBackend().getJingleFile(message);
240			content.setFileOffer(this.file);
241			this.transportId = this.mJingleConnectionManager.nextRandomId();
242			content.setTransportId(this.transportId);
243			content.socks5transport().setChildren(getCandidatesAsElements());
244			packet.setContent(content);
245			this.sendJinglePacket(packet);
246			this.status = STATUS_INITIATED;
247		}
248	}
249	
250	private List<Element> getCandidatesAsElements() {
251		List<Element> elements = new ArrayList<Element>();
252		for(JingleCandidate c : this.candidates) {
253			elements.add(c.toElement());
254		}
255		return elements;
256	}
257	
258	private void sendAccept() {
259		status = STATUS_ACCEPTED;
260		mXmppConnectionService.markMessage(message, Message.STATUS_RECIEVING);
261		this.mJingleConnectionManager.getPrimaryCandidate(this.account, new OnPrimaryCandidateFound() {
262			
263			@Override
264			public void onPrimaryCandidateFound(boolean success,final JingleCandidate candidate) {
265				final JinglePacket packet = bootstrapPacket("session-accept");
266				final Content content = new Content();
267				content.setFileOffer(fileOffer);
268				content.setTransportId(transportId);
269				if ((success)&&(!equalCandidateExists(candidate))) {
270					final JingleSocks5Transport socksConnection = new JingleSocks5Transport(JingleConnection.this, candidate);
271					connections.put(candidate.getCid(), socksConnection);
272					socksConnection.connect(new OnTransportConnected() {
273						
274						@Override
275						public void failed() {
276							Log.d("xmppService","connection to our own primary candidate failed");
277							content.socks5transport().setChildren(getCandidatesAsElements());
278							packet.setContent(content);
279							sendJinglePacket(packet);
280							connectNextCandidate();
281						}
282						
283						@Override
284						public void established() {
285							Log.d("xmppService","connected to primary candidate");
286							mergeCandidate(candidate);
287							content.socks5transport().setChildren(getCandidatesAsElements());
288							packet.setContent(content);
289							sendJinglePacket(packet);
290							connectNextCandidate();
291						}
292					});
293				} else {
294					Log.d("xmppService","did not find a primary candidate for ourself");
295					content.socks5transport().setChildren(getCandidatesAsElements());
296					packet.setContent(content);
297					sendJinglePacket(packet);
298					connectNextCandidate();
299				}
300			}
301		});
302		
303	}
304	
305	private JinglePacket bootstrapPacket(String action) {
306		JinglePacket packet = new JinglePacket();
307		packet.setAction(action);
308		packet.setFrom(account.getFullJid());
309		packet.setTo(this.message.getCounterpart());
310		packet.setSessionId(this.sessionId);
311		packet.setInitiator(this.initiator);
312		return packet;
313	}
314	
315	private void sendJinglePacket(JinglePacket packet) {
316		//Log.d("xmppService",packet.toString());
317		account.getXmppConnection().sendIqPacket(packet,responseListener);
318	}
319	
320	private void receiveAccept(JinglePacket packet) {
321		Content content = packet.getJingleContent();
322		mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren()));
323		this.status = STATUS_ACCEPTED;
324		mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
325		this.connectNextCandidate();
326		IqPacket response = packet.generateRespone(IqPacket.TYPE_RESULT);
327		account.getXmppConnection().sendIqPacket(response, null);
328	}
329
330	private void receiveTransportInfo(JinglePacket packet) {
331		Content content = packet.getJingleContent();
332		if (content.hasSocks5Transport()) {
333			if (content.socks5transport().hasChild("activated")) {
334				onProxyActivated.success();
335			} else if (content.socks5transport().hasChild("activated")) {
336				onProxyActivated.failed();
337			} else if (content.socks5transport().hasChild("candidate-error")) {
338				Log.d("xmppService","received candidate error");
339				this.receivedCandidate = true;
340				if (status == STATUS_ACCEPTED) {
341					this.connect();
342				}
343			} else if (content.socks5transport().hasChild("candidate-used")){
344				String cid = content.socks5transport().findChild("candidate-used").getAttribute("cid");
345				if (cid!=null) {
346					Log.d("xmppService","candidate used by counterpart:"+cid);
347					JingleCandidate candidate = getCandidate(cid);
348					candidate.flagAsUsedByCounterpart();
349					this.receivedCandidate = true;
350					if ((status == STATUS_ACCEPTED)&&(this.sentCandidate)) {
351						this.connect();
352					} else {
353						Log.d("xmppService","ignoring because file is already in transmission or we havent sent our candidate yet");
354					}
355				} else {
356					Log.d("xmppService","couldn't read used candidate");
357				}
358			} else {
359				Log.d("xmppService","empty transport");
360			}
361		}
362		
363		IqPacket response = packet.generateRespone(IqPacket.TYPE_RESULT);
364		account.getXmppConnection().sendIqPacket(response, null);
365	}
366
367	private void connect() {
368		final JingleSocks5Transport connection = chooseConnection();
369		this.transport = connection;
370		if (connection==null) {
371			Log.d("xmppService","could not find suitable candidate");
372			this.disconnect();
373			if (this.initiator.equals(account.getFullJid())) {
374				this.sendFallbackToIbb();
375			}
376		} else {
377			this.status = STATUS_TRANSMITTING;
378			if (connection.isProxy()) {
379				if (connection.getCandidate().isOurs()) {
380					Log.d("xmppService","candidate "+connection.getCandidate().getCid()+" was our proxy and needs activation");
381					IqPacket activation = new IqPacket(IqPacket.TYPE_SET);
382					activation.setTo(connection.getCandidate().getJid());
383					activation.query("http://jabber.org/protocol/bytestreams").setAttribute("sid", this.getSessionId());
384					activation.query().addChild("activate").setContent(this.getCounterPart());
385					this.account.getXmppConnection().sendIqPacket(activation, new OnIqPacketReceived() {
386						
387						@Override
388						public void onIqPacketReceived(Account account, IqPacket packet) {
389							if (packet.getType()==IqPacket.TYPE_ERROR) {
390								onProxyActivated.failed();
391							} else {
392								onProxyActivated.success();
393								sendProxyActivated(connection.getCandidate().getCid());
394							}
395						}
396					});
397				}
398			} else {
399				if (initiator.equals(account.getFullJid())) {
400					Log.d("xmppService","we were initiating. sending file");
401					connection.send(file,onFileTransmitted);
402				} else {
403					Log.d("xmppService","we were responding. receiving file");
404					connection.receive(file,onFileTransmitted);
405				}
406			}
407		}
408	}
409	
410	private JingleSocks5Transport chooseConnection() {
411		JingleSocks5Transport connection = null;
412		Iterator<Entry<String, JingleSocks5Transport>> it = this.connections.entrySet().iterator();
413	    while (it.hasNext()) {
414	    	Entry<String, JingleSocks5Transport> pairs = it.next();
415	    	JingleSocks5Transport currentConnection = pairs.getValue();
416	    	//Log.d("xmppService","comparing candidate: "+currentConnection.getCandidate().toString());
417	        if (currentConnection.isEstablished()&&(currentConnection.getCandidate().isUsedByCounterpart()||(!currentConnection.getCandidate().isOurs()))) {
418	        	//Log.d("xmppService","is usable");
419	        	if (connection==null) {
420	        		connection = currentConnection;
421	        	} else {
422	        		if (connection.getCandidate().getPriority()<currentConnection.getCandidate().getPriority()) {
423	        			connection = currentConnection;
424	        		} else if (connection.getCandidate().getPriority()==currentConnection.getCandidate().getPriority()) {
425	        			//Log.d("xmppService","found two candidates with same priority");
426	        			if (initiator.equals(account.getFullJid())) {
427	        				if (currentConnection.getCandidate().isOurs()) {
428	        					connection = currentConnection;
429	        				}
430	        			} else {
431	        				if (!currentConnection.getCandidate().isOurs()) {
432	        					connection = currentConnection;
433	        				}
434	        			}
435	        		}
436	        	}
437	        }
438	        it.remove();
439	    }
440		return connection;
441	}
442
443	private void sendSuccess() {
444		JinglePacket packet = bootstrapPacket("session-terminate");
445		Reason reason = new Reason();
446		reason.addChild("success");
447		packet.setReason(reason);
448		this.sendJinglePacket(packet);
449		this.disconnect();
450		this.status = STATUS_FINISHED;
451		this.mXmppConnectionService.markMessage(this.message, Message.STATUS_RECIEVED);
452	}
453	
454	private void sendFallbackToIbb() {
455		JinglePacket packet = this.bootstrapPacket("transport-replace");
456		Content content = new Content("initiator","a-file-offer");
457		this.transportId = this.mJingleConnectionManager.nextRandomId();
458		content.setTransportId(this.transportId);
459		content.ibbTransport().setAttribute("block-size",""+this.ibbBlockSize);
460		packet.setContent(content);
461		this.sendJinglePacket(packet);
462	}
463	
464	private void receiveFallbackToIbb(JinglePacket packet) {
465		String receivedBlockSize = packet.getJingleContent().ibbTransport().getAttribute("block-size");
466		if (receivedBlockSize!=null) {
467			int bs = Integer.parseInt(receivedBlockSize);
468			if (bs>this.ibbBlockSize) {
469				this.ibbBlockSize = bs;
470			}
471		}
472		this.transportId = packet.getJingleContent().getTransportId();
473		this.transport = new JingleInbandTransport(this.account,this.responder,this.transportId,this.ibbBlockSize);
474		this.transport.receive(file, onFileTransmitted);
475		JinglePacket answer = bootstrapPacket("transport-accept");
476		Content content = new Content("initiator", "a-file-offer");
477		content.setTransportId(this.transportId);
478		content.ibbTransport().setAttribute("block-size", ""+this.ibbBlockSize);
479		answer.setContent(content);
480		this.sendJinglePacket(answer);
481	}
482	
483	private void receiveTransportAccept(JinglePacket packet) {
484		if (packet.getJingleContent().hasIbbTransport()) {
485			String receivedBlockSize = packet.getJingleContent().ibbTransport().getAttribute("block-size");
486			if (receivedBlockSize!=null) {
487				int bs = Integer.parseInt(receivedBlockSize);
488				if (bs>this.ibbBlockSize) {
489					this.ibbBlockSize = bs;
490				}
491			}
492			this.transport = new JingleInbandTransport(this.account,this.responder,this.transportId,this.ibbBlockSize);
493			this.transport.connect(new OnTransportConnected() {
494				
495				@Override
496				public void failed() {
497					Log.d("xmppService","ibb open failed");
498				}
499				
500				@Override
501				public void established() {
502					JingleConnection.this.transport.send(file, onFileTransmitted);
503				}
504			});
505		} else {
506			Log.d("xmppService","invalid transport accept");
507		}
508	}
509	
510	private void finish() {
511		this.status = STATUS_FINISHED;
512		this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND);
513		this.disconnect();
514	}
515	
516	public void cancel() {
517		this.disconnect();
518		this.status = STATUS_CANCELED;
519		this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND_REJECTED);
520	}
521
522	private void connectNextCandidate() {
523		for(JingleCandidate candidate : this.candidates) {
524			if ((!connections.containsKey(candidate.getCid())&&(!candidate.isOurs()))) {
525				this.connectWithCandidate(candidate);
526				return;
527			}
528		}
529		this.sendCandidateError();
530	}
531	
532	private void connectWithCandidate(final JingleCandidate candidate) {
533		final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this,candidate);
534		connections.put(candidate.getCid(), socksConnection);
535		socksConnection.connect(new OnTransportConnected() {
536			
537			@Override
538			public void failed() {
539				Log.d("xmppService", "connection failed with "+candidate.getHost()+":"+candidate.getPort());
540				connectNextCandidate();
541			}
542			
543			@Override
544			public void established() {
545				Log.d("xmppService", "established connection with "+candidate.getHost()+":"+candidate.getPort());
546				sendCandidateUsed(candidate.getCid());
547			}
548		});
549	}
550
551	private void disconnect() {
552		Iterator<Entry<String, JingleSocks5Transport>> it = this.connections.entrySet().iterator();
553	    while (it.hasNext()) {
554	        Entry<String, JingleSocks5Transport> pairs = it.next();
555	        pairs.getValue().disconnect();
556	        it.remove();
557	    }
558	}
559	
560	private void sendProxyActivated(String cid) {
561		JinglePacket packet = bootstrapPacket("transport-info");
562		Content content = new Content("inititaor","a-file-offer");
563		content.setTransportId(this.transportId);
564		content.socks5transport().addChild("activated").setAttribute("cid", cid);
565		packet.setContent(content);
566		this.sendJinglePacket(packet);
567	}
568	
569	private void sendCandidateUsed(final String cid) {
570		JinglePacket packet = bootstrapPacket("transport-info");
571		Content content = new Content("initiator","a-file-offer");
572		content.setTransportId(this.transportId);
573		content.socks5transport().addChild("candidate-used").setAttribute("cid", cid);
574		packet.setContent(content);
575		this.sentCandidate = true;
576		if ((receivedCandidate)&&(status == STATUS_ACCEPTED)) {
577			connect();
578		}
579		this.sendJinglePacket(packet);
580	}
581	
582	private void sendCandidateError() {
583		JinglePacket packet = bootstrapPacket("transport-info");
584		Content content = new Content("initiator","a-file-offer");
585		content.setTransportId(this.transportId);
586		content.socks5transport().addChild("candidate-error");
587		packet.setContent(content);
588		this.sentCandidate = true;
589		if ((receivedCandidate)&&(status == STATUS_ACCEPTED)) {
590			connect();
591		}
592		this.sendJinglePacket(packet);
593	}
594
595	public String getInitiator() {
596		return this.initiator;
597	}
598	
599	public String getResponder() {
600		return this.responder;
601	}
602	
603	public int getStatus() {
604		return this.status;
605	}
606	
607	private boolean equalCandidateExists(JingleCandidate candidate) {
608		for(JingleCandidate c : this.candidates) {
609			if (c.equalValues(candidate)) {
610				return true;
611			}
612		}
613		return false;
614	}
615	
616	private void mergeCandidate(JingleCandidate candidate) {
617		for(JingleCandidate c : this.candidates) {
618			if (c.equals(candidate)) {
619				return;
620			}
621		}
622		this.candidates.add(candidate);
623	}
624	
625	private void mergeCandidates(List<JingleCandidate> candidates) {
626		for(JingleCandidate c : candidates) {
627			mergeCandidate(c);
628		}
629	}
630	
631	private JingleCandidate getCandidate(String cid) {
632		for(JingleCandidate c : this.candidates) {
633			if (c.getCid().equals(cid)) {
634				return c;
635			}
636		}
637		return null;
638	}
639	
640	interface OnProxyActivated {
641		public void success();
642		public void failed();
643	}
644
645	public boolean hasTransportId(String sid) {
646		return sid.equals(this.transportId);
647	}
648	
649	public JingleTransport getTransport() {
650		return this.transport;
651	}
652
653	public void accept() {
654		if (status==STATUS_INITIATED) {
655			new Thread(new Runnable() {
656				
657				@Override
658				public void run() {
659					sendAccept();
660				}
661			}).start();
662		} else {
663			Log.d("xmppService","status ("+status+") was not ok");
664		}
665	}
666}