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 status = -1;
 36	private Message message;
 37	private String sessionId;
 38	private Account account;
 39	private String initiator;
 40	private String responder;
 41	private List<Element> candidates = new ArrayList<Element>();
 42	private List<String> candidatesUsedByCounterpart = new ArrayList<String>();
 43	private HashMap<String, SocksConnection> connections = new HashMap<String, SocksConnection>();
 44	private Content content = new Content();
 45	private JingleFile file = null;
 46	
 47	private OnIqPacketReceived responseListener = new OnIqPacketReceived() {
 48		
 49		@Override
 50		public void onIqPacketReceived(Account account, IqPacket packet) {
 51			if (packet.getType() == IqPacket.TYPE_ERROR) {
 52				mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
 53				status = STATUS_FAILED;
 54			}
 55		}
 56	};
 57	
 58	public JingleConnection(JingleConnectionManager mJingleConnectionManager) {
 59		this.mJingleConnectionManager = mJingleConnectionManager;
 60		this.mXmppConnectionService = mJingleConnectionManager.getXmppConnectionService();
 61	}
 62	
 63	public String getSessionId() {
 64		return this.sessionId;
 65	}
 66	
 67	public String getAccountJid() {
 68		return this.account.getFullJid();
 69	}
 70	
 71	public String getCounterPart() {
 72		return this.message.getCounterpart();
 73	}
 74	
 75	public void deliverPacket(JinglePacket packet) {
 76		
 77		if (packet.isAction("session-terminate")) {
 78			Reason reason = packet.getReason();
 79			if (reason.hasChild("cancel")) {
 80				this.cancel();
 81			} else if (reason.hasChild("success")) {
 82				this.finish();
 83			}
 84		} else if (packet.isAction("session-accept")) {
 85			accept(packet);
 86		} else if (packet.isAction("transport-info")) {
 87			transportInfo(packet);
 88		} else {
 89			Log.d("xmppService","packet arrived in connection. action was "+packet.getAction());
 90		}
 91	}
 92	
 93	public void init(Message message) {
 94		this.message = message;
 95		this.account = message.getConversation().getAccount();
 96		this.initiator = this.account.getFullJid();
 97		this.responder = this.message.getCounterpart();
 98		this.sessionId = this.mJingleConnectionManager.nextRandomId();
 99		if (this.candidates.size() > 0) {
100			this.sendInitRequest();
101		} else {
102			this.mJingleConnectionManager.getPrimaryCandidate(account, new OnPrimaryCandidateFound() {
103				
104				@Override
105				public void onPrimaryCandidateFound(boolean success, Element candidate) {
106					if (success) {
107						mergeCandidate(candidate);
108					}
109					sendInitRequest();
110				}
111			});
112		}
113		
114	}
115	
116	public void init(Account account, JinglePacket packet) {
117		this.status = STATUS_INITIATED;
118		Conversation conversation = this.mXmppConnectionService.findOrCreateConversation(account, packet.getFrom().split("/")[0], false);
119		this.message = new Message(conversation, "receiving image file", Message.ENCRYPTION_NONE);
120		this.message.setType(Message.TYPE_IMAGE);
121		this.message.setStatus(Message.STATUS_RECIEVING);
122		String[] fromParts = packet.getFrom().split("/");
123		this.message.setPresence(fromParts[1]);
124		this.account = account;
125		this.initiator = packet.getFrom();
126		this.responder = this.account.getFullJid();
127		this.sessionId = packet.getSessionId();
128		this.content = packet.getJingleContent();
129		this.mergeCandidates(this.content.getCanditates());
130		Element fileOffer = packet.getJingleContent().getFileOffer();
131		if (fileOffer!=null) {
132			this.file = this.mXmppConnectionService.getFileBackend().getJingleFile(message);
133			Element fileSize = fileOffer.findChild("size");
134			Element fileName = fileOffer.findChild("name");
135			this.file.setExpectedSize(Long.parseLong(fileSize.getContent()));
136			if (this.file.getExpectedSize()>=this.mJingleConnectionManager.getAutoAcceptFileSize()) {
137				Log.d("xmppService","auto accepting file from "+packet.getFrom());
138				this.sendAccept();
139			} else {
140				Log.d("xmppService","not auto accepting new file offer with size: "+this.file.getExpectedSize()+" allowed size:"+this.mJingleConnectionManager.getAutoAcceptFileSize());
141			}
142		} else {
143			Log.d("xmppService","no file offer was attached. aborting");
144		}
145		Log.d("xmppService","session Id "+getSessionId());
146	}
147	
148	private void sendInitRequest() {
149		JinglePacket packet = this.bootstrapPacket();
150		packet.setAction("session-initiate");
151		this.content = new Content();
152		if (message.getType() == Message.TYPE_IMAGE) {
153			content.setAttribute("creator", "initiator");
154			content.setAttribute("name", "a-file-offer");
155			this.file = this.mXmppConnectionService.getFileBackend().getJingleFile(message);
156			content.setFileOffer(this.file);
157			content.setCandidates(this.mJingleConnectionManager.nextRandomId(),this.candidates);
158			packet.setContent(content);
159			Log.d("xmppService",packet.toString());
160			account.getXmppConnection().sendIqPacket(packet, this.responseListener);
161			this.status = STATUS_INITIATED;
162		}
163	}
164	
165	private void sendAccept() {
166		this.mJingleConnectionManager.getPrimaryCandidate(this.account, new OnPrimaryCandidateFound() {
167			
168			@Override
169			public void onPrimaryCandidateFound(boolean success, Element candidate) {
170				if (success) {
171					if (mergeCandidate(candidate)) {
172						content.addCandidate(candidate);
173					}
174				}
175				JinglePacket packet = bootstrapPacket();
176				packet.setAction("session-accept");
177				packet.setContent(content);
178				account.getXmppConnection().sendIqPacket(packet, new OnIqPacketReceived() {
179					
180					@Override
181					public void onIqPacketReceived(Account account, IqPacket packet) {
182						if (packet.getType() != IqPacket.TYPE_ERROR) {
183							status = STATUS_ACCEPTED;
184							connectWithCandidates();
185						}
186					}
187				});
188			}
189		});
190		
191	}
192	
193	private JinglePacket bootstrapPacket() {
194		JinglePacket packet = new JinglePacket();
195		packet.setFrom(account.getFullJid());
196		packet.setTo(this.message.getCounterpart()); //fixme, not right in all cases;
197		packet.setSessionId(this.sessionId);
198		packet.setInitiator(this.initiator);
199		return packet;
200	}
201	
202	private void accept(JinglePacket packet) {
203		Log.d("xmppService","session-accept: "+packet.toString());
204		Content content = packet.getJingleContent();
205		this.mergeCandidates(content.getCanditates());
206		this.status = STATUS_ACCEPTED;
207		this.connectWithCandidates();
208		IqPacket response = packet.generateRespone(IqPacket.TYPE_RESULT);
209		account.getXmppConnection().sendIqPacket(response, null);
210	}
211
212	private void transportInfo(JinglePacket packet) {
213		Content content = packet.getJingleContent();
214		String cid = content.getUsedCandidate();
215		IqPacket response = packet.generateRespone(IqPacket.TYPE_RESULT);
216		if (cid!=null) {
217			Log.d("xmppService","candidate used by counterpart:"+cid);
218			this.candidatesUsedByCounterpart.add(cid);
219			if (this.connections.containsKey(cid)) {
220				SocksConnection connection = this.connections.get(cid);
221				if (connection.isEstablished()) {
222					if (status!=STATUS_TRANSMITTING) {
223						this.connect(connection);
224					} else {
225						Log.d("xmppService","ignoring canditate used because we are already transmitting");
226					}
227				} else {
228					Log.d("xmppService","not yet connected. check when callback comes back");
229				}
230			} else {
231				Log.d("xmppService","candidate not yet in list of connections");
232			}
233		}
234		account.getXmppConnection().sendIqPacket(response, null);
235	}
236
237	private void connect(final SocksConnection connection) {
238		this.status = STATUS_TRANSMITTING;
239		final OnFileTransmitted callback = new OnFileTransmitted() {
240			
241			@Override
242			public void onFileTransmitted(JingleFile file) {
243				Log.d("xmppService","sucessfully transmitted file. sha1:"+file.getSha1Sum());
244			}
245		};
246		if ((connection.isProxy()&&(connection.getCid().equals(mJingleConnectionManager.getPrimaryCandidateId(account))))) {
247			Log.d("xmppService","candidate "+connection.getCid()+" was our proxy and needs activation");
248			IqPacket activation = new IqPacket(IqPacket.TYPE_SET);
249			activation.setTo(connection.getJid());
250			activation.query("http://jabber.org/protocol/bytestreams").setAttribute("sid", this.getSessionId());
251			activation.query().addChild("activate").setContent(this.getResponder());
252			this.account.getXmppConnection().sendIqPacket(activation, new OnIqPacketReceived() {
253				
254				@Override
255				public void onIqPacketReceived(Account account, IqPacket packet) {
256					Log.d("xmppService","activation result: "+packet.toString());
257					if (initiator.equals(account.getFullJid())) {
258						Log.d("xmppService","we were initiating. sending file");
259						connection.send(file,callback);
260					} else {
261						connection.receive(file,callback);
262						Log.d("xmppService","we were responding. receiving file");
263					}
264				}
265			});
266		} else {
267			if (initiator.equals(account.getFullJid())) {
268				Log.d("xmppService","we were initiating. sending file");
269				connection.send(file,callback);
270			} else {
271				Log.d("xmppService","we were responding. receiving file");
272				connection.receive(file,callback);
273			}
274		}
275	}
276	
277	private void finish() {
278		this.status = STATUS_FINISHED;
279		this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND);
280		this.disconnect();
281	}
282	
283	public void cancel() {
284		this.disconnect();
285		this.status = STATUS_CANCELED;
286		this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND_REJECTED);
287	}
288	
289	private void connectWithCandidates() {
290		for(Element candidate : this.candidates) {
291			final SocksConnection socksConnection = new SocksConnection(this,candidate);
292			connections.put(socksConnection.getCid(), socksConnection);
293			socksConnection.connect(new OnSocksConnection() {
294				
295				@Override
296				public void failed() {
297					Log.d("xmppService","socks5 failed");
298				}
299				
300				@Override
301				public void established() {
302					if (candidatesUsedByCounterpart.contains(socksConnection.getCid())) {
303						if (status!=STATUS_TRANSMITTING) {
304							connect(socksConnection);
305						} else {
306							Log.d("xmppService","ignoring cuz already transmitting");
307						}
308					} else {
309						sendCandidateUsed(socksConnection.getCid());
310					}
311				}
312			});
313		}
314	}
315	
316	private void disconnect() {
317		Iterator<Entry<String, SocksConnection>> it = this.connections.entrySet().iterator();
318	    while (it.hasNext()) {
319	        Entry<String, SocksConnection> pairs = it.next();
320	        pairs.getValue().disconnect();
321	        it.remove();
322	    }
323	}
324	
325	private void sendCandidateUsed(String cid) {
326		JinglePacket packet = bootstrapPacket();
327		packet.setAction("transport-info");
328		Content content = new Content();
329		content.setUsedCandidate(this.content.getTransportId(), cid);
330		packet.setContent(content);
331		Log.d("xmppService","send using candidate: "+packet.toString());
332		this.account.getXmppConnection().sendIqPacket(packet, responseListener);
333	}
334
335	public String getInitiator() {
336		return this.initiator;
337	}
338	
339	public String getResponder() {
340		return this.responder;
341	}
342	
343	public int getStatus() {
344		return this.status;
345	}
346	
347	private boolean mergeCandidate(Element candidate) {
348		for(Element c : this.candidates) {
349			if (c.getAttribute("host").equals(candidate.getAttribute("host"))&&(c.getAttribute("port").equals(candidate.getAttribute("port")))) {
350				return false;
351			}
352		}
353		this.candidates.add(candidate);
354		return true;
355	}
356	
357	private void mergeCandidates(List<Element> canditates) {
358		for(Element c : canditates) {
359			this.mergeCandidate(c);
360		}
361	}
362}