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