diff --git a/CHANGELOG.md b/CHANGELOG.md index 56d2bf476..73f0f42f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Version 2.5.8 +* fixed connection issues over Tor +* P2P file transfer (Jingle) now offers direct candidates +* Support XEP-0396: Jingle Encrypted Transports - OMEMO + ### Version 2.5.7 * fixed crash when scanning QR codes on Android 6 and lower * when sharing a message from and to Conversations insert it as quote diff --git a/build.gradle b/build.gradle index dbc1a9495..1eed97d8f 100644 --- a/build.gradle +++ b/build.gradle @@ -83,8 +83,8 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 25 - versionCode 337 - versionName "2.5.7" + versionCode 338 + versionName "2.5.8" archivesBaseName += "-$versionName" applicationId "eu.sum7.conversations" resValue "string", "applicationId", applicationId diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index d64c81e1d..2c9f207d2 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -101,6 +101,7 @@ public final class Config { public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb + public static final boolean USE_DIRECT_JINGLE_CANDIDATES = true; public static final boolean DISABLE_HTTP_UPLOAD = false; public static final boolean EXTENDED_SM_LOGGING = false; // log stanza counts public static final boolean BACKGROUND_STANZA_LOGGING = false; //log all stanzas that were received while the app is in background diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java index 1270c1019..5f93400e2 100644 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -29,6 +29,8 @@ public abstract class AbstractGenerator { Content.Version.FT_5.getNamespace(), Namespace.JINGLE_TRANSPORTS_S5B, Namespace.JINGLE_TRANSPORTS_IBB, + Namespace.JINGLE_ENCRYPTED_TRANSPORT, + Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO, "http://jabber.org/protocol/muc", "jabber:x:conference", Namespace.OOB, diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 8c77bdad4..509d4b193 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -39,6 +39,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.ServerSocket; import java.net.Socket; import java.net.URL; import java.security.DigestOutputStream; @@ -359,6 +360,15 @@ public class FileBackend { } } + public static void close(final ServerSocket socket) { + if (socket != null) { + try { + socket.close(); + } catch (IOException e) { + } + } + } + public static boolean weOwnFile(Context context, Uri uri) { if (uri == null || !ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { return false; diff --git a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java index 432c70390..43c28b854 100644 --- a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java @@ -26,6 +26,7 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.utils.Compatibility; +import eu.siacs.conversations.utils.CryptoHelper; public class AbstractConnectionManager { diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index d6b9fc108..7cc3f807a 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -11,7 +11,6 @@ import android.content.SharedPreferences; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Typeface; -import android.graphics.drawable.Icon; import android.media.AudioAttributes; import android.media.RingtoneManager; import android.net.Uri; @@ -23,7 +22,6 @@ import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat.BigPictureStyle; import android.support.v4.app.NotificationCompat.Builder; import android.support.v4.app.NotificationManagerCompat; -import android.support.v4.app.NotificationCompat.CarExtender.UnreadConversation; import android.support.v4.app.Person; import android.support.v4.app.RemoteInput; import android.support.v4.content.ContextCompat; @@ -32,7 +30,6 @@ import android.text.SpannableString; import android.text.style.StyleSpan; import android.util.DisplayMetrics; import android.util.Log; -import android.util.Pair; import java.io.File; import java.io.IOException; @@ -64,7 +61,6 @@ import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.XmppConnection; -import rocks.xmpp.addr.Jid; public class NotificationService { @@ -469,10 +465,7 @@ public class NotificationService { private Builder buildMultipleConversation(final boolean notify, final boolean quietHours) { final Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService, quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages")); final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(); - style.setBigContentTitle(notifications.size() - + " " - + mXmppConnectionService - .getString(R.string.unread_conversations)); + style.setBigContentTitle(mXmppConnectionService.getString(R.string.x_unread_conversations,notifications.size())); final StringBuilder names = new StringBuilder(); Conversation conversation = null; for (final ArrayList messages : notifications.values()) { @@ -497,10 +490,8 @@ public class NotificationService { if (names.length() >= 2) { names.delete(names.length() - 2, names.length()); } - mBuilder.setContentTitle(notifications.size() - + " " - + mXmppConnectionService - .getString(R.string.unread_conversations)); + mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.x_unread_conversations, notifications.size())); + mBuilder.setTicker(mXmppConnectionService.getString(R.string.x_unread_conversations, notifications.size())); mBuilder.setContentText(names.toString()); mBuilder.setStyle(style); if (conversation != null) { @@ -627,8 +618,11 @@ public class NotificationService { CharSequence text = getMergedBodies(tmp); bigPictureStyle.setSummaryText(text); builder.setContentText(text); + builder.setTicker(text); } else { - builder.setContentText(UIHelper.getFileDescriptionString(mXmppConnectionService, message)); + final String description = UIHelper.getFileDescriptionString(mXmppConnectionService, message); + builder.setContentText(description); + builder.setTicker(description); } builder.setStyle(bigPictureStyle); } catch (final IOException e) { @@ -685,7 +679,9 @@ public class NotificationService { } else { if (messages.get(0).getConversation().getMode() == Conversation.MODE_SINGLE) { builder.setStyle(new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages))); - builder.setContentText(UIHelper.getMessagePreview(mXmppConnectionService, messages.get(messages.size()-1)).first); + final CharSequence preview = UIHelper.getMessagePreview(mXmppConnectionService, messages.get(messages.size()-1)).first; + builder.setContentText(preview); + builder.setTicker(preview); builder.setNumber(messages.size()); } else { final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(); @@ -703,8 +699,11 @@ public class NotificationService { styledString = new SpannableString(name + ": " + messages.get(0).getBody()); styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0); builder.setContentText(styledString); + builder.setTicker(styledString); } else { - builder.setContentText(mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count)); + final String text = mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count); + builder.setContentText(text); + builder.setTicker(text); } } } diff --git a/src/main/java/eu/siacs/conversations/utils/Resolver.java b/src/main/java/eu/siacs/conversations/utils/Resolver.java index 32961c6d8..0b913bd39 100644 --- a/src/main/java/eu/siacs/conversations/utils/Resolver.java +++ b/src/main/java/eu/siacs/conversations/utils/Resolver.java @@ -66,12 +66,16 @@ public class Resolver { Result result = new Result(); result.hostname = DNSName.from(hostname); result.port = port; - result.directTls = port == 443 || port == 5223; + result.directTls = useDirectTls(port); result.authenticated = true; return Collections.singletonList(result); } + public static boolean useDirectTls(final int port) { + return port == 443 || port == 5223; + } + public static List resolve(String domain) { final List ipResults = fromIpAddress(domain); if (ipResults.size() > 0) { diff --git a/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java b/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java index 81f936538..48e0c1d55 100644 --- a/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java +++ b/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java @@ -20,6 +20,9 @@ public class SocksSocketFactory { proxyOs.write(new byte[]{0x05, 0x01, 0x00}); byte[] response = new byte[2]; proxyIs.read(response); + if (response[0] != 0x05 || response[1] != 0x00) { + throw new SocksConnectionException("Socks 5 handshake failed"); + } byte[] dest = destination.getBytes(); ByteBuffer request = ByteBuffer.allocate(7 + dest.length); request.put(new byte[]{0x05, 0x01, 0x00, 0x03}); @@ -30,10 +33,25 @@ public class SocksSocketFactory { response = new byte[7 + dest.length]; proxyIs.read(response); if (response[1] != 0x00) { - throw new SocksConnectionException(); + if (response[1] == 0x04) { + throw new HostNotFoundException("Host unreachable"); + } + if (response[1] == 0x05) { + throw new HostNotFoundException("Connection refused"); + } + throw new SocksConnectionException("Unable to connect to destination "+(int) (response[1])); } } + public static boolean contains(byte needle, byte[] haystack) { + for(byte hay : haystack) { + if (hay == needle) { + return true; + } + } + return false; + } + public static Socket createSocket(InetSocketAddress address, String destination, int port) throws IOException { Socket socket = new Socket(); try { @@ -49,11 +67,19 @@ public class SocksSocketFactory { return createSocket(new InetSocketAddress(InetAddress.getByAddress(LOCALHOST), 9050), destination, port); } - static class SocksConnectionException extends IOException { - + private static class SocksConnectionException extends IOException { + SocksConnectionException(String message) { + super(message); + } } public static class SocksProxyNotFoundException extends IOException { } + + public static class HostNotFoundException extends SocksConnectionException { + HostNotFoundException(String message) { + super(message); + } + } } diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index dbab039d7..55d54853e 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -30,4 +30,6 @@ public final class Namespace { public static final String PING = "urn:xmpp:ping"; public static final String PUSH = "urn:xmpp:push:0"; public static final String COMMANDS = "http://jabber.org/protocol/commands"; + public static final String JINGLE_ENCRYPTED_TRANSPORT = "urn:xmpp:jingle:jet:0"; + public static final String JINGLE_ENCRYPTED_TRANSPORT_OMEMO = "urn:xmpp:jingle:jet-omemo:0"; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 6b1f95998..702a8c6ca 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -267,8 +267,18 @@ public class XmppConnection implements Runnable { destination = account.getHostname(); this.verifiedHostname = destination; } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": connect to " + destination + " via Tor"); - localSocket = SocksSocketFactory.createSocketOverTor(destination, account.getPort()); + + final int port = account.getPort(); + final boolean directTls = Resolver.useDirectTls(port); + + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": connect to " + destination + " via Tor. directTls="+directTls); + localSocket = SocksSocketFactory.createSocketOverTor(destination, port); + + if (directTls) { + localSocket = upgradeSocketToTls(localSocket); + features.encryptionEnabled = true; + } + try { startXmpp(localSocket); } catch (InterruptedException e) { @@ -310,29 +320,13 @@ public class XmppConnection implements Runnable { + ": using values from resolver " + result.getHostname().toString() + ":" + result.getPort() + " tls: " + features.encryptionEnabled); - if (!features.encryptionEnabled) { - localSocket = new Socket(); - localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000); - } else { - final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier(); - localSocket = tlsFactoryVerifier.factory.createSocket(); + localSocket = new Socket(); + localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000); - if (localSocket == null) { - throw new IOException("could not initialize ssl socket"); - } - - SSLSocketHelper.setSecurity((SSLSocket) localSocket); - SSLSocketHelper.setHostname((SSLSocket) localSocket, account.getServer()); - SSLSocketHelper.setApplicationProtocol((SSLSocket) localSocket, "xmpp-client"); - - localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000); - - if (!tlsFactoryVerifier.verifier.verify(account.getServer(), verifiedHostname, ((SSLSocket) localSocket).getSession())) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS certificate verification failed"); - FileBackend.close(localSocket); - throw new StateChangingException(Account.State.TLS_ERROR); - } + if (features.encryptionEnabled) { + localSocket = upgradeSocketToTls(localSocket); } + localSocket.setSoTimeout(Config.SOCKET_TIMEOUT * 1000); if (startXmpp(localSocket)) { localSocket.setSoTimeout(0); //reset to 0; once the connection is established we don’t want this @@ -363,6 +357,8 @@ public class XmppConnection implements Runnable { this.changeStatus(e.state); } catch (final UnknownHostException | ConnectException e) { this.changeStatus(Account.State.SERVER_NOT_FOUND); + } catch (final SocksSocketFactory.HostNotFoundException e) { + this.changeStatus(Account.State.SERVER_NOT_FOUND); } catch (final SocksSocketFactory.SocksProxyNotFoundException e) { this.changeStatus(Account.State.TOR_NOT_AVAILABLE); } catch (final IOException | XmlPullParserException e) { @@ -775,46 +771,41 @@ public class XmppConnection implements Runnable { private void switchOverToTls() throws XmlPullParserException, IOException { tagReader.readTag(); + final Socket socket = this.socket; + final SSLSocket sslSocket = upgradeSocketToTls(socket); + tagReader.setInputStream(sslSocket.getInputStream()); + tagWriter.setOutputStream(sslSocket.getOutputStream()); + sendStartStream(); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS connection established"); + features.encryptionEnabled = true; + final Tag tag = tagReader.readTag(); + if (tag != null && tag.isStart("stream")) { + SSLSocketHelper.log(account, sslSocket); + processStream(); + } else { + throw new StateChangingException(Account.State.STREAM_OPENING_ERROR); + } + sslSocket.close(); + } + + private SSLSocket upgradeSocketToTls(final Socket socket) throws IOException { + final TlsFactoryVerifier tlsFactoryVerifier; try { - final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier(); - final InetAddress address = socket == null ? null : socket.getInetAddress(); - - if (address == null) { - throw new IOException("could not setup ssl"); - } - - final SSLSocket sslSocket = (SSLSocket) tlsFactoryVerifier.factory.createSocket(socket, address.getHostAddress(), socket.getPort(), true); - - - if (sslSocket == null) { - throw new IOException("could not initialize ssl socket"); - } - - SSLSocketHelper.setSecurity(sslSocket); - SSLSocketHelper.setHostname(sslSocket, account.getServer()); - SSLSocketHelper.setApplicationProtocol(sslSocket, "xmpp-client"); - - if (!tlsFactoryVerifier.verifier.verify(account.getServer(), this.verifiedHostname, sslSocket.getSession())) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS certificate verification failed"); - throw new StateChangingException(Account.State.TLS_ERROR); - } - tagReader.setInputStream(sslSocket.getInputStream()); - tagWriter.setOutputStream(sslSocket.getOutputStream()); - sendStartStream(); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS connection established"); - features.encryptionEnabled = true; - final Tag tag = tagReader.readTag(); - if (tag != null && tag.isStart("stream")) { - SSLSocketHelper.log(account, sslSocket); - processStream(); - } else { - throw new StateChangingException(Account.State.STREAM_OPENING_ERROR); - } - sslSocket.close(); - } catch (final NoSuchAlgorithmException | KeyManagementException e1) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS certificate verification failed"); + tlsFactoryVerifier = getTlsFactoryVerifier(); + } catch (final NoSuchAlgorithmException | KeyManagementException e) { throw new StateChangingException(Account.State.TLS_ERROR); } + final InetAddress address = socket.getInetAddress(); + final SSLSocket sslSocket = (SSLSocket) tlsFactoryVerifier.factory.createSocket(socket, address.getHostAddress(), socket.getPort(), true); + SSLSocketHelper.setSecurity(sslSocket); + SSLSocketHelper.setHostname(sslSocket, account.getServer()); + SSLSocketHelper.setApplicationProtocol(sslSocket, "xmpp-client"); + if (!tlsFactoryVerifier.verifier.verify(account.getServer(), this.verifiedHostname, sslSocket.getSession())) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS certificate verification failed"); + FileBackend.close(sslSocket); + throw new StateChangingException(Account.State.TLS_ERROR); + } + return sslSocket; } private void processStreamFeatures(final Tag currentTag) throws XmlPullParserException, IOException { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/DirectConnectionUtils.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/DirectConnectionUtils.java new file mode 100644 index 000000000..8d9818ed9 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/DirectConnectionUtils.java @@ -0,0 +1,64 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.UUID; + +import rocks.xmpp.addr.Jid; + +public class DirectConnectionUtils { + + private static List getLocalAddresses() { + final List addresses = new ArrayList<>(); + final Enumeration interfaces; + try { + interfaces = NetworkInterface.getNetworkInterfaces(); + } catch (SocketException e) { + return addresses; + } + while (interfaces.hasMoreElements()) { + NetworkInterface networkInterface = interfaces.nextElement(); + final Enumeration inetAddressEnumeration = networkInterface.getInetAddresses(); + while (inetAddressEnumeration.hasMoreElements()) { + final InetAddress inetAddress = inetAddressEnumeration.nextElement(); + if (inetAddress.isLoopbackAddress() || inetAddress.isLinkLocalAddress()) { + continue; + } + if (inetAddress instanceof Inet6Address) { + //let's get rid of scope + try { + addresses.add(Inet6Address.getByAddress(inetAddress.getAddress())); + } catch (UnknownHostException e) { + //ignored + } + } else { + addresses.add(inetAddress); + } + } + } + return addresses; + } + + public static List getLocalCandidates(Jid jid) { + SecureRandom random = new SecureRandom(); + ArrayList candidates = new ArrayList<>(); + for (InetAddress inetAddress : getLocalAddresses()) { + final JingleCandidate candidate = new JingleCandidate(UUID.randomUUID().toString(), true); + candidate.setHost(inetAddress.getHostAddress()); + candidate.setPort(random.nextInt(60000) + 1024); + candidate.setType(JingleCandidate.TYPE_DIRECT); + candidate.setJid(jid); + candidate.setPriority(8257536 + candidates.size()); + candidates.add(candidate); + } + return candidates; + } + +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java index d48804691..7415c32aa 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java @@ -110,15 +110,12 @@ public class JingleCandidate { } public static JingleCandidate parse(Element candidate) { - JingleCandidate parsedCandidate = new JingleCandidate( - candidate.getAttribute("cid"), false); + JingleCandidate parsedCandidate = new JingleCandidate(candidate.getAttribute("cid"), false); parsedCandidate.setHost(candidate.getAttribute("host")); parsedCandidate.setJid(InvalidJid.getNullForInvalid(candidate.getAttributeAsJid("jid"))); parsedCandidate.setType(candidate.getAttribute("type")); - parsedCandidate.setPriority(Integer.parseInt(candidate - .getAttribute("priority"))); - parsedCandidate - .setPort(Integer.parseInt(candidate.getAttribute("port"))); + parsedCandidate.setPriority(Integer.parseInt(candidate.getAttribute("priority"))); + parsedCandidate.setPort(Integer.parseInt(candidate.getAttribute("port"))); return parsedCandidate; } @@ -127,7 +124,9 @@ public class JingleCandidate { element.setAttribute("cid", this.getCid()); element.setAttribute("host", this.getHost()); element.setAttribute("port", Integer.toString(this.getPort())); - element.setAttribute("jid", this.getJid().toString()); + if (jid != null) { + element.setAttribute("jid", jid.toEscapedString()); + } element.setAttribute("priority", Integer.toString(this.getPriority())); if (this.getType() == TYPE_DIRECT) { element.setAttribute("type", "direct"); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java index d06c2469f..919350ec8 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java @@ -43,6 +43,8 @@ import rocks.xmpp.addr.Jid; public class JingleConnection implements Transferable { + private static final String JET_OMEMO_CIPHER = "urn:xmpp:ciphers:aes-128-gcm-nopadding"; + private static final int JINGLE_STATUS_INITIATED = 0; private static final int JINGLE_STATUS_ACCEPTED = 1; private static final int JINGLE_STATUS_FINISHED = 4; @@ -72,6 +74,7 @@ public class JingleConnection implements Transferable { private String contentName; private String contentCreator; private Transport initialTransport; + private boolean remoteSupportsOmemoJet; private int mProgress = 0; @@ -171,7 +174,10 @@ public class JingleConnection implements Transferable { @Override public void failed() { - Log.d(Config.LOGTAG, "proxy activation failed"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": proxy activation failed"); + if (initiating()) { + sendFallbackToIbb(); + } } }; @@ -292,8 +298,10 @@ public class JingleConnection implements Transferable { this.contentName = this.mJingleConnectionManager.nextRandomId(); this.message = message; this.account = message.getConversation().getAccount(); - upgradeNamespace(); - this.initialTransport = getRemoteFeatures().contains(Namespace.JINGLE_TRANSPORTS_S5B) ? Transport.SOCKS : Transport.IBB; + final List remoteFeatures = getRemoteFeatures(); + upgradeNamespace(remoteFeatures); + this.initialTransport = remoteFeatures.contains(Namespace.JINGLE_TRANSPORTS_S5B) ? Transport.SOCKS : Transport.IBB; + this.remoteSupportsOmemoJet = remoteFeatures.contains(Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO); this.message.setTransferable(this); this.mStatus = Transferable.STATUS_UPLOADING; this.initiator = this.account.getJid(); @@ -302,10 +310,9 @@ public class JingleConnection implements Transferable { this.transportId = this.mJingleConnectionManager.nextRandomId(); if (this.initialTransport == Transport.IBB) { this.sendInitRequest(); - } else if (this.candidates.size() > 0) { - this.sendInitRequest(); } else { - this.mJingleConnectionManager.getPrimaryCandidate(account, (success, candidate) -> { + gatherAndConnectDirectCandidates(); + this.mJingleConnectionManager.getPrimaryCandidate(account, initiating(), (success, candidate) -> { if (success) { final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate); connections.put(candidate.getCid(), socksConnection); @@ -313,22 +320,20 @@ public class JingleConnection implements Transferable { @Override public void failed() { - Log.d(Config.LOGTAG, - "connection to our own primary candidete failed"); + Log.d(Config.LOGTAG, "connection to our own proxy65 candidate failed"); sendInitRequest(); } @Override public void established() { - Log.d(Config.LOGTAG, - "successfully connected to our own primary candidate"); + Log.d(Config.LOGTAG, "successfully connected to our own proxy65 candidate"); mergeCandidate(candidate); sendInitRequest(); } }); mergeCandidate(candidate); } else { - Log.d(Config.LOGTAG, "no primary candidate of our own was found"); + Log.d(Config.LOGTAG, "no proxy65 candidate of our own was found"); sendInitRequest(); } }); @@ -336,11 +341,28 @@ public class JingleConnection implements Transferable { } - private void upgradeNamespace() { - List features = getRemoteFeatures(); - if (features.contains(Content.Version.FT_5.getNamespace())) { + private void gatherAndConnectDirectCandidates() { + final List directCandidates; + if (Config.USE_DIRECT_JINGLE_CANDIDATES) { + if (account.isOnion() || mXmppConnectionService.useTorToConnect()) { + directCandidates = Collections.emptyList(); + } else { + directCandidates = DirectConnectionUtils.getLocalCandidates(account.getJid()); + } + } else { + directCandidates = Collections.emptyList(); + } + for (JingleCandidate directCandidate : directCandidates) { + final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, directCandidate); + connections.put(directCandidate.getCid(), socksConnection); + candidates.add(directCandidate); + } + } + + private void upgradeNamespace(List remoteFeatures) { + if (remoteFeatures.contains(Content.Version.FT_5.getNamespace())) { this.ftVersion = Content.Version.FT_5; - } else if (features.contains(Content.Version.FT_4.getNamespace())) { + } else if (remoteFeatures.contains(Content.Version.FT_4.getNamespace())) { this.ftVersion = Content.Version.FT_4; } } @@ -408,8 +430,18 @@ public class JingleConnection implements Transferable { } this.fileOffer = content.getFileOffer(this.ftVersion); + if (fileOffer != null) { + boolean remoteIsUsingJet = false; Element encrypted = fileOffer.findChild("encrypted", AxolotlService.PEP_PREFIX); + if (encrypted == null) { + final Element security = content.findChild("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT); + if (security != null && AxolotlService.PEP_PREFIX.equals(security.getAttribute("type"))) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received jingle file offer with JET"); + encrypted = security.findChild("encrypted", AxolotlService.PEP_PREFIX); + remoteIsUsingJet = true; + } + } if (encrypted != null) { this.mXmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, packet.getFrom().asBareJid()); } @@ -450,7 +482,10 @@ public class JingleConnection implements Transferable { } } message.resetFileParams(); - this.file.setExpectedSize(size); + //legacy OMEMO encrypted file transfers reported the file size after encryption + //JET reports the plain text size. however lower levels of our receiving code still + //expect the cipher text size. so we just + 16 bytes (auth tag size) here + this.file.setExpectedSize(size + (remoteIsUsingJet ? 16 : 0)); if (mJingleConnectionManager.hasStoragePermission() && size < this.mJingleConnectionManager.getAutoAcceptFileSize() && mXmppConnectionService.isDataSaverDisabled()) { @@ -499,8 +534,21 @@ public class JingleConnection implements Transferable { if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { this.file.setKey(mXmppAxolotlMessage.getInnerKey()); this.file.setIv(mXmppAxolotlMessage.getIV()); - this.file.setExpectedSize(file.getSize() + 16); - content.setFileOffer(this.file, false, this.ftVersion).addChild(mXmppAxolotlMessage.toElement()); + //legacy OMEMO encrypted file transfer reported file size of the encrypted file + //JET uses the file size of the plain text file. The difference is only 16 bytes (auth tag) + this.file.setExpectedSize(file.getSize() + (this.remoteSupportsOmemoJet ? 0 : 16)); + final Element file = content.setFileOffer(this.file, false, this.ftVersion); + if (remoteSupportsOmemoJet) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": remote announced support for JET"); + final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT); + security.setAttribute("name", this.contentName); + security.setAttribute("cipher", JET_OMEMO_CIPHER); + security.setAttribute("type", AxolotlService.PEP_PREFIX); + security.addChild(mXmppAxolotlMessage.toElement()); + content.addChild(security); + } else { + file.addChild(mXmppAxolotlMessage.toElement()); + } } else { this.file.setExpectedSize(file.getSize()); content.setFileOffer(this.file, false, this.ftVersion); @@ -564,7 +612,8 @@ public class JingleConnection implements Transferable { } private void sendAcceptSocks() { - this.mJingleConnectionManager.getPrimaryCandidate(this.account, (success, candidate) -> { + gatherAndConnectDirectCandidates(); + this.mJingleConnectionManager.getPrimaryCandidate(this.account, initiating(), (success, candidate) -> { final JinglePacket packet = bootstrapPacket("session-accept"); final Content content = new Content(contentCreator, contentName); content.setFileOffer(fileOffer, ftVersion); @@ -576,7 +625,7 @@ public class JingleConnection implements Transferable { @Override public void failed() { - Log.d(Config.LOGTAG, "connection to our own primary candidate failed"); + Log.d(Config.LOGTAG, "connection to our own proxy65 candidate failed"); content.socks5transport().setChildren(getCandidatesAsElements()); packet.setContent(content); sendJinglePacket(packet); @@ -585,7 +634,7 @@ public class JingleConnection implements Transferable { @Override public void established() { - Log.d(Config.LOGTAG, "connected to primary candidate"); + Log.d(Config.LOGTAG, "connected to proxy65 candidate"); mergeCandidate(candidate); content.socks5transport().setChildren(getCandidatesAsElements()); packet.setContent(content); @@ -594,7 +643,7 @@ public class JingleConnection implements Transferable { } }); } else { - Log.d(Config.LOGTAG, "did not find a primary candidate for ourself"); + Log.d(Config.LOGTAG, "did not find a proxy65 candidate for ourselves"); content.socks5transport().setChildren(getCandidatesAsElements()); packet.setContent(content); sendJinglePacket(packet); @@ -635,7 +684,7 @@ public class JingleConnection implements Transferable { private boolean receiveAccept(JinglePacket packet) { if (this.mJingleStatus != JINGLE_STATUS_INITIATED) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": received out of order session-accept"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received out of order session-accept"); return false; } this.mJingleStatus = JINGLE_STATUS_ACCEPTED; @@ -654,7 +703,7 @@ public class JingleConnection implements Transferable { this.ibbBlockSize = bs; } } catch (Exception e) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": unable to parse block size in session-accept"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to parse block size in session-accept"); } } this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize); @@ -689,7 +738,7 @@ public class JingleConnection implements Transferable { onProxyActivated.failed(); return true; } else if (content.socks5transport().hasChild("candidate-error")) { - Log.d(Config.LOGTAG, "received candidate error"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received candidate error"); this.receivedCandidate = true; if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) { this.connect(); @@ -727,12 +776,14 @@ public class JingleConnection implements Transferable { final JingleSocks5Transport connection = chooseConnection(); this.transport = connection; if (connection == null) { - Log.d(Config.LOGTAG, "could not find suitable candidate"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not find suitable candidate"); this.disconnectSocks5Connections(); if (initiating()) { this.sendFallbackToIbb(); } } else { + final JingleCandidate candidate = connection.getCandidate(); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": elected candidate " + candidate.getHost() + ":" + candidate.getPort()); this.mJingleStatus = JINGLE_STATUS_TRANSMITTING; if (connection.needsActivation()) { if (connection.getCandidate().isOurs()) { @@ -754,10 +805,12 @@ public class JingleConnection implements Transferable { .setContent(this.getCounterPart().toString()); mXmppConnectionService.sendIqPacket(account, activation, (account, response) -> { if (response.getType() != IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + response.toString()); + sendProxyError(); onProxyActivated.failed(); } else { - onProxyActivated.success(); sendProxyActivated(connection.getCandidate().getCid()); + onProxyActivated.success(); } }); } else { @@ -841,7 +894,7 @@ public class JingleConnection implements Transferable { private boolean receiveFallbackToIbb(JinglePacket packet) { - Log.d(Config.LOGTAG, "receiving fallback to ibb"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": receiving fallback to ibb"); final String receivedBlockSize = packet.getJingleContent().ibbTransport().getAttribute("block-size"); if (receivedBlockSize != null) { try { @@ -850,7 +903,7 @@ public class JingleConnection implements Transferable { this.ibbBlockSize = bs; } } catch (NumberFormatException e) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": unable to parse block size in transport-replace"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to parse block size in transport-replace"); } } this.transportId = packet.getJingleContent().getTransportId(); @@ -889,7 +942,7 @@ public class JingleConnection implements Transferable { this.ibbBlockSize = bs; } } catch (NumberFormatException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid()+": unable to parse block size in transport-accept"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to parse block size in transport-accept"); } } this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize); @@ -1029,14 +1082,23 @@ public class JingleConnection implements Transferable { } private void sendProxyActivated(String cid) { - JinglePacket packet = bootstrapPacket("transport-info"); - Content content = new Content(this.contentCreator, this.contentName); + final JinglePacket packet = bootstrapPacket("transport-info"); + final Content content = new Content(this.contentCreator, this.contentName); content.setTransportId(this.transportId); content.socks5transport().addChild("activated").setAttribute("cid", cid); packet.setContent(content); this.sendJinglePacket(packet); } + private void sendProxyError() { + final JinglePacket packet = bootstrapPacket("transport-info"); + final Content content = new Content(this.contentCreator, this.contentName); + content.setTransportId(this.transportId); + content.socks5transport().addChild("proxy-error"); + packet.setContent(content); + this.sendJinglePacket(packet); + } + private void sendCandidateUsed(final String cid) { JinglePacket packet = bootstrapPacket("transport-info"); Content content = new Content(this.contentCreator, this.contentName); @@ -1051,7 +1113,7 @@ public class JingleConnection implements Transferable { } private void sendCandidateError() { - Log.d(Config.LOGTAG, "sending candidate error"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending candidate error"); JinglePacket packet = bootstrapPacket("transport-info"); Content content = new Content(this.contentCreator, this.contentName); content.setTransportId(this.transportId); @@ -1087,6 +1149,7 @@ public class JingleConnection implements Transferable { } private void mergeCandidates(List candidates) { + Collections.sort(candidates, (a, b) -> Integer.compare(b.getPriority(), a.getPriority())); for (JingleCandidate c : candidates) { mergeCandidate(c); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 237fd1229..30057f532 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -81,8 +81,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { this.connections.remove(connection); } - public void getPrimaryCandidate(Account account, - final OnPrimaryCandidateFound listener) { + public void getPrimaryCandidate(final Account account, final boolean initiator, final OnPrimaryCandidateFound listener) { if (Config.DISABLE_PROXY_LOOKUP) { listener.onPrimaryCandidateFound(false, null); return; @@ -107,7 +106,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { candidate.setPort(Integer.parseInt(port)); candidate.setType(JingleCandidate.TYPE_PROXY); candidate.setJid(proxy); - candidate.setPriority(655360 + 65535); + candidate.setPriority(655360 + (initiator ? 10 : 20)); primaryCandidates.put(account.getJid().asBareJid(),candidate); listener.onPrimaryCandidateFound(true,candidate); } catch (final NumberFormatException e) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java index 99c12ba3a..41aa75ad9 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java @@ -6,13 +6,17 @@ import android.util.Log; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.InetAddress; import java.net.InetSocketAddress; +import java.net.ServerSocket; import java.net.Socket; import java.net.SocketAddress; +import java.nio.ByteBuffer; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.AbstractConnectionManager; @@ -22,177 +26,268 @@ import eu.siacs.conversations.utils.WakeLockHelper; import eu.siacs.conversations.xmpp.jingle.stanzas.Content; public class JingleSocks5Transport extends JingleTransport { - private JingleCandidate candidate; - private JingleConnection connection; - private String destination; - private OutputStream outputStream; - private InputStream inputStream; - private boolean isEstablished = false; - private boolean activated = false; - private Socket socket; + private final JingleCandidate candidate; + private final JingleConnection connection; + private final String destination; + private OutputStream outputStream; + private InputStream inputStream; + private boolean isEstablished = false; + private boolean activated = false; + private ServerSocket serverSocket; + private Socket socket; - JingleSocks5Transport(JingleConnection jingleConnection, JingleCandidate candidate) { - this.candidate = candidate; - this.connection = jingleConnection; - try { - MessageDigest mDigest = MessageDigest.getInstance("SHA-1"); - StringBuilder destBuilder = new StringBuilder(); - if (jingleConnection.getFtVersion() == Content.Version.FT_3) { - Log.d(Config.LOGTAG, this.connection.getAccount().getJid().asBareJid() + ": using session Id instead of transport Id for proxy destination"); - destBuilder.append(jingleConnection.getSessionId()); - } else { - destBuilder.append(jingleConnection.getTransportId()); - } - if (candidate.isOurs()) { - destBuilder.append(jingleConnection.getAccount().getJid()); - destBuilder.append(jingleConnection.getCounterPart()); - } else { - destBuilder.append(jingleConnection.getCounterPart()); - destBuilder.append(jingleConnection.getAccount().getJid()); - } - mDigest.reset(); - this.destination = CryptoHelper.bytesToHex(mDigest - .digest(destBuilder.toString().getBytes())); - } catch (NoSuchAlgorithmException e) { + JingleSocks5Transport(JingleConnection jingleConnection, JingleCandidate candidate) { + final MessageDigest messageDigest; + try { + messageDigest = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + this.candidate = candidate; + this.connection = jingleConnection; + final StringBuilder destBuilder = new StringBuilder(); + if (jingleConnection.getFtVersion() == Content.Version.FT_3) { + Log.d(Config.LOGTAG, this.connection.getAccount().getJid().asBareJid() + ": using session Id instead of transport Id for proxy destination"); + destBuilder.append(jingleConnection.getSessionId()); + } else { + destBuilder.append(jingleConnection.getTransportId()); + } + if (candidate.isOurs()) { + destBuilder.append(jingleConnection.getAccount().getJid()); + destBuilder.append(jingleConnection.getCounterPart()); + } else { + destBuilder.append(jingleConnection.getCounterPart()); + destBuilder.append(jingleConnection.getAccount().getJid()); + } + messageDigest.reset(); + this.destination = CryptoHelper.bytesToHex(messageDigest.digest(destBuilder.toString().getBytes())); + if (candidate.isOurs() && candidate.getType() == JingleCandidate.TYPE_DIRECT) { + createServerSocket(); + } + } - } - } + private void createServerSocket() { + try { + serverSocket = new ServerSocket(); + serverSocket.bind(new InetSocketAddress(InetAddress.getByName(candidate.getHost()), candidate.getPort())); + new Thread(() -> { + try { + final Socket socket = serverSocket.accept(); + new Thread(() -> { + try { + acceptIncomingSocketConnection(socket); + } catch (IOException e) { + Log.d(Config.LOGTAG, "unable to read from socket", e); - public void connect(final OnTransportConnected callback) { - new Thread(() -> { - try { - final boolean useTor = connection.getAccount().isOnion() || connection.getConnectionManager().getXmppConnectionService().useTorToConnect(); - if (useTor) { - socket = SocksSocketFactory.createSocketOverTor(candidate.getHost(), candidate.getPort()); - } else { - socket = new Socket(); - SocketAddress address = new InetSocketAddress(candidate.getHost(), candidate.getPort()); - socket.connect(address, Config.SOCKET_TIMEOUT * 1000); - } - inputStream = socket.getInputStream(); - outputStream = socket.getOutputStream(); - SocksSocketFactory.createSocksConnection(socket, destination, 0); - isEstablished = true; - callback.established(); - } catch (IOException e) { - callback.failed(); - } - }).start(); + } + }).start(); + } catch (IOException e) { + if (!serverSocket.isClosed()) { + Log.d(Config.LOGTAG, "unable to accept socket", e); + } + } + }).start(); + } catch (IOException e) { + Log.d(Config.LOGTAG, "unable to bind server socket ", e); + } + } - } + private void acceptIncomingSocketConnection(Socket socket) throws IOException { + Log.d(Config.LOGTAG, "accepted connection from " + socket.getInetAddress().getHostAddress()); + final byte[] authBegin = new byte[2]; + final InputStream inputStream = socket.getInputStream(); + final OutputStream outputStream = socket.getOutputStream(); + inputStream.read(authBegin); + if (authBegin[0] != 0x5) { + socket.close(); + } + final short methodCount = authBegin[1]; + final byte[] methods = new byte[methodCount]; + inputStream.read(methods); + if (SocksSocketFactory.contains((byte) 0x00, methods)) { + outputStream.write(new byte[]{0x05, 0x00}); + } else { + outputStream.write(new byte[]{0x05, (byte) 0xff}); + } + byte[] connectCommand = new byte[4]; + inputStream.read(connectCommand); + if (connectCommand[0] == 0x05 && connectCommand[1] == 0x01 && connectCommand[3] == 0x03) { + int destinationCount = inputStream.read(); + final byte[] destination = new byte[destinationCount]; + inputStream.read(destination); + final int port = inputStream.read(); + final String receivedDestination = new String(destination); + final ByteBuffer response = ByteBuffer.allocate(7 + destination.length); + final byte[] responseHeader; + final boolean success; + if (receivedDestination.equals(this.destination) && this.socket == null) { + responseHeader = new byte[]{0x05, 0x00, 0x00, 0x03}; + success = true; + } else { + Log.d(Config.LOGTAG,connection.getAccount().getJid().asBareJid()+": destination mismatch. received "+receivedDestination+" (expected "+this.destination+")"); + responseHeader = new byte[]{0x05, 0x04, 0x00, 0x03}; + success = false; + } + response.put(responseHeader); + response.put((byte) destination.length); + response.put(destination); + response.putShort((short) port); + outputStream.write(response.array()); + outputStream.flush(); + if (success) { + Log.d(Config.LOGTAG,connection.getAccount().getJid().asBareJid()+": successfully processed connection to candidate "+candidate.getHost()+":"+candidate.getPort()); + this.socket = socket; + this.inputStream = inputStream; + this.outputStream = outputStream; + this.isEstablished = true; + FileBackend.close(serverSocket); + } else { + this.socket.close(); + } + } else { + socket.close(); + } + } - public void send(final DownloadableFile file, final OnFileTransmissionStatusChanged callback) { - new Thread(() -> { - InputStream fileInputStream = null; - final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_send_" + connection.getSessionId()); - try { - wakeLock.acquire(); - MessageDigest digest = MessageDigest.getInstance("SHA-1"); - digest.reset(); - fileInputStream = connection.getFileInputStream(); - if (fileInputStream == null) { - Log.d(Config.LOGTAG, connection.getAccount().getJid().asBareJid() + ": could not create input stream"); - callback.onFileTransferAborted(); - return; - } - final InputStream innerInputStream = AbstractConnectionManager.upgrade(file, fileInputStream); - long size = file.getExpectedSize(); - long transmitted = 0; - int count; - byte[] buffer = new byte[8192]; - while ((count = innerInputStream.read(buffer)) > 0) { - outputStream.write(buffer, 0, count); - digest.update(buffer, 0, count); - transmitted += count; - connection.updateProgress((int) ((((double) transmitted) / size) * 100)); - } - outputStream.flush(); - file.setSha1Sum(digest.digest()); - if (callback != null) { - callback.onFileTransmitted(file); - } - } catch (Exception e) { - Log.d(Config.LOGTAG, connection.getAccount().getJid().asBareJid() + ": " + e.getMessage()); - callback.onFileTransferAborted(); - } finally { - FileBackend.close(fileInputStream); - WakeLockHelper.release(wakeLock); - } - }).start(); + public void connect(final OnTransportConnected callback) { + new Thread(() -> { + try { + final boolean useTor = connection.getAccount().isOnion() || connection.getConnectionManager().getXmppConnectionService().useTorToConnect(); + if (useTor) { + socket = SocksSocketFactory.createSocketOverTor(candidate.getHost(), candidate.getPort()); + } else { + socket = new Socket(); + SocketAddress address = new InetSocketAddress(candidate.getHost(), candidate.getPort()); + socket.connect(address, 5000); + } + inputStream = socket.getInputStream(); + outputStream = socket.getOutputStream(); + socket.setSoTimeout(5000); + SocksSocketFactory.createSocksConnection(socket, destination, 0); + socket.setSoTimeout(0); + isEstablished = true; + callback.established(); + } catch (IOException e) { + callback.failed(); + } + }).start(); - } + } - public void receive(final DownloadableFile file, final OnFileTransmissionStatusChanged callback) { - new Thread(() -> { - OutputStream fileOutputStream = null; - final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_receive_" + connection.getSessionId()); - try { - wakeLock.acquire(); - MessageDigest digest = MessageDigest.getInstance("SHA-1"); - digest.reset(); - //inputStream.skip(45); - socket.setSoTimeout(30000); - fileOutputStream = connection.getFileOutputStream(); - if (fileOutputStream == null) { - callback.onFileTransferAborted(); - Log.d(Config.LOGTAG, connection.getAccount().getJid().asBareJid() + ": could not create output stream"); - return; - } - double size = file.getExpectedSize(); - long remainingSize = file.getExpectedSize(); - byte[] buffer = new byte[8192]; - int count; - while (remainingSize > 0) { - count = inputStream.read(buffer); - if (count == -1) { - callback.onFileTransferAborted(); - Log.d(Config.LOGTAG, connection.getAccount().getJid().asBareJid() + ": file ended prematurely with " + remainingSize + " bytes remaining"); - return; - } else { - fileOutputStream.write(buffer, 0, count); - digest.update(buffer, 0, count); - remainingSize -= count; - } - connection.updateProgress((int) (((size - remainingSize) / size) * 100)); - } - fileOutputStream.flush(); - fileOutputStream.close(); - file.setSha1Sum(digest.digest()); - callback.onFileTransmitted(file); - } catch (Exception e) { - Log.d(Config.LOGTAG, connection.getAccount().getJid().asBareJid() + ": " + e.getMessage()); - callback.onFileTransferAborted(); - } finally { - WakeLockHelper.release(wakeLock); - FileBackend.close(fileOutputStream); - FileBackend.close(inputStream); - } - }).start(); - } + public void send(final DownloadableFile file, final OnFileTransmissionStatusChanged callback) { + new Thread(() -> { + InputStream fileInputStream = null; + final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_send_" + connection.getSessionId()); + long transmitted = 0; + try { + wakeLock.acquire(); + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.reset(); + fileInputStream = connection.getFileInputStream(); + if (fileInputStream == null) { + Log.d(Config.LOGTAG, connection.getAccount().getJid().asBareJid() + ": could not create input stream"); + callback.onFileTransferAborted(); + return; + } + final InputStream innerInputStream = AbstractConnectionManager.upgrade(file, fileInputStream); + long size = file.getExpectedSize(); + int count; + byte[] buffer = new byte[8192]; + while ((count = innerInputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, count); + digest.update(buffer, 0, count); + transmitted += count; + connection.updateProgress((int) ((((double) transmitted) / size) * 100)); + } + outputStream.flush(); + file.setSha1Sum(digest.digest()); + if (callback != null) { + callback.onFileTransmitted(file); + } + } catch (Exception e) { + final Account account = connection.getAccount(); + Log.d(Config.LOGTAG, account.getJid().asBareJid()+": failed sending file after "+transmitted+"/"+file.getExpectedSize()+" ("+ socket.getInetAddress()+":"+socket.getPort()+")", e); + callback.onFileTransferAborted(); + } finally { + FileBackend.close(fileInputStream); + WakeLockHelper.release(wakeLock); + } + }).start(); - public boolean isProxy() { - return this.candidate.getType() == JingleCandidate.TYPE_PROXY; - } + } - public boolean needsActivation() { - return (this.isProxy() && !this.activated); - } + public void receive(final DownloadableFile file, final OnFileTransmissionStatusChanged callback) { + new Thread(() -> { + OutputStream fileOutputStream = null; + final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_receive_" + connection.getSessionId()); + try { + wakeLock.acquire(); + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.reset(); + //inputStream.skip(45); + socket.setSoTimeout(30000); + fileOutputStream = connection.getFileOutputStream(); + if (fileOutputStream == null) { + callback.onFileTransferAborted(); + Log.d(Config.LOGTAG, connection.getAccount().getJid().asBareJid() + ": could not create output stream"); + return; + } + double size = file.getExpectedSize(); + long remainingSize = file.getExpectedSize(); + byte[] buffer = new byte[8192]; + int count; + while (remainingSize > 0) { + count = inputStream.read(buffer); + if (count == -1) { + callback.onFileTransferAborted(); + Log.d(Config.LOGTAG, connection.getAccount().getJid().asBareJid() + ": file ended prematurely with " + remainingSize + " bytes remaining"); + return; + } else { + fileOutputStream.write(buffer, 0, count); + digest.update(buffer, 0, count); + remainingSize -= count; + } + connection.updateProgress((int) (((size - remainingSize) / size) * 100)); + } + fileOutputStream.flush(); + fileOutputStream.close(); + file.setSha1Sum(digest.digest()); + callback.onFileTransmitted(file); + } catch (Exception e) { + Log.d(Config.LOGTAG, connection.getAccount().getJid().asBareJid() + ": " + e.getMessage()); + callback.onFileTransferAborted(); + } finally { + WakeLockHelper.release(wakeLock); + FileBackend.close(fileOutputStream); + FileBackend.close(inputStream); + } + }).start(); + } - public void disconnect() { - FileBackend.close(inputStream); - FileBackend.close(outputStream); - FileBackend.close(socket); - } + public boolean isProxy() { + return this.candidate.getType() == JingleCandidate.TYPE_PROXY; + } - public boolean isEstablished() { - return this.isEstablished; - } + public boolean needsActivation() { + return (this.isProxy() && !this.activated); + } - public JingleCandidate getCandidate() { - return this.candidate; - } + public void disconnect() { + FileBackend.close(inputStream); + FileBackend.close(outputStream); + FileBackend.close(socket); + FileBackend.close(serverSocket); + } - public void setActivated(boolean activated) { - this.activated = activated; - } + public boolean isEstablished() { + return this.isEstablished; + } + + public JingleCandidate getCandidate() { + return this.candidate; + } + + public void setActivated(boolean activated) { + this.activated = activated; + } } diff --git a/src/main/res/values-ar/strings.xml b/src/main/res/values-ar/strings.xml index 589fd2dcc..63960d682 100644 --- a/src/main/res/values-ar/strings.xml +++ b/src/main/res/values-ar/strings.xml @@ -27,7 +27,6 @@ الآن منذ 1 دقيقة دقائق %d منذ - محادثات غير مقروءة ارسال حل شيفرة الرسالة. الرجاء الإنتظار ... رسالة مشفرة عبر OpenPGP diff --git a/src/main/res/values-bg/strings.xml b/src/main/res/values-bg/strings.xml index 1d51c5d48..6106d4dfa 100644 --- a/src/main/res/values-bg/strings.xml +++ b/src/main/res/values-bg/strings.xml @@ -28,7 +28,6 @@ току-що преди 1 минута преди %d минути - непрочетени разговори изпращане… Дешифроване на съобщението. Моля, изчакайте… Съобщение, шифр. чрез OpenPGP diff --git a/src/main/res/values-ca/strings.xml b/src/main/res/values-ca/strings.xml index e65553f95..4996652ee 100644 --- a/src/main/res/values-ca/strings.xml +++ b/src/main/res/values-ca/strings.xml @@ -27,7 +27,6 @@ Ara fa 1 min fa %d mins - Converses sense llegir o no llegides enviant… Desxifrant el missatge. Espereu… Missatge xifrat amb OpenPGP diff --git a/src/main/res/values-cs/strings.xml b/src/main/res/values-cs/strings.xml index 2fc61851a..6e2c444e6 100644 --- a/src/main/res/values-cs/strings.xml +++ b/src/main/res/values-cs/strings.xml @@ -21,7 +21,6 @@ právě teď před minutou před %d minutami - nepřečtené konverzace odesílám… Dešifrování zprávy. Chvíli strpení... OpenPGP šifrovaná zpráva diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 8fba03091..dae6138a0 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -30,7 +30,7 @@ gerade vor einer Minute vor %d Minuten - ungelesene Unterhaltungen + %d ungelesene Unterhaltungen senden… Nachricht wird entschlüsselt. Bitte warten … OpenPGP-verschlüsselte Nachricht diff --git a/src/main/res/values-el/strings.xml b/src/main/res/values-el/strings.xml index 126c7f901..07e002163 100644 --- a/src/main/res/values-el/strings.xml +++ b/src/main/res/values-el/strings.xml @@ -30,7 +30,6 @@ μόλις τώρα πριν από 1 λεπτό πριν από %d λεπτά - μη αναγνωσμένες Συζητήσεις αποστολή... Αποκρυπτογράφηση μηνύματος. Παρακαλώ περιμένετε... OpenPGP κρυπτογραφημένο μήνυμα diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index a67508cce..a5b0fb18d 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -30,7 +30,6 @@ ahora hace 1 min hace %d min - conversaciones por leer enviando… Descifrando mensaje. Por favor, espera... Mensaje cifrado con OpenPGP diff --git a/src/main/res/values-eu/strings.xml b/src/main/res/values-eu/strings.xml index 2d3bc1e2d..ab15680fb 100644 --- a/src/main/res/values-eu/strings.xml +++ b/src/main/res/values-eu/strings.xml @@ -30,7 +30,6 @@ orain min 1 lehenago %d min lehenago - irakurri gabeko elkarrizketak bidaltzen… Mezua desenkriptatzen. Mesedez itxaron… OpenPGPz enkriptatutako mezua diff --git a/src/main/res/values-fa-rIR/strings.xml b/src/main/res/values-fa-rIR/strings.xml index e7ff22809..d70439f87 100644 --- a/src/main/res/values-fa-rIR/strings.xml +++ b/src/main/res/values-fa-rIR/strings.xml @@ -25,7 +25,6 @@ هم اکنون 1 دقیقه قبل %d دقیقه قبل - گفتگو های خوانده نشده در حال ارسال... در حال رمزگشایی پیام. لطفا صبور باشید... پیام رمز شده به وسیله OpenPGP diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index b2c5032a8..c19899d79 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -28,7 +28,6 @@ À l\'instant Il y a 1 minute Il y a %d minutes - Conversations non lues Envoi… Déchiffrement du message. Veuillez patienter... Message chiffré avec OpenPGP diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index f4d9c6864..d0471a226 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -30,7 +30,7 @@ agora Hai 1 min hai %d minutos - conversas sen ler + %d conversas non lidas enviando… Descifrando a mensaxe. Por favor agarde... Mensaxe cifrado con OpenPGP diff --git a/src/main/res/values-hu/strings.xml b/src/main/res/values-hu/strings.xml index af1a9acff..2a0ecfe3c 100644 --- a/src/main/res/values-hu/strings.xml +++ b/src/main/res/values-hu/strings.xml @@ -30,7 +30,6 @@ Éppen most 1 perce %d perce - olvasatlan beszélgetés küldés... Üzenet dekódolása. Kérem várjon... OpenPGP kódolású üzenet @@ -871,4 +870,5 @@ A kiválasztott fájl nem a Conversations biztonsági mentése Ez a fiók már be lett állítva Kérem, adja meg a fiókhoz tartozó jelszót + Nem sikerült ezt a cselekvést elvégezni diff --git a/src/main/res/values-id/strings.xml b/src/main/res/values-id/strings.xml index 3e25146c9..7359773be 100644 --- a/src/main/res/values-id/strings.xml +++ b/src/main/res/values-id/strings.xml @@ -21,7 +21,6 @@ sekarang 1 min lalu %d min lalu - Percakapan belum dibaca mengirim... Mendekripsi pesan. Mohon tunggu… Pesan terenkripsi OpenPGP diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index aaf7a75e6..e001699d7 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -30,7 +30,6 @@ adesso 1 min fa %d min fa - Conversazioni non lette invio… Decifrazione messaggio. Attendere prego... Messaggio cifrato con OpenPGP diff --git a/src/main/res/values-iw/strings.xml b/src/main/res/values-iw/strings.xml index 93f55608d..3a5ad007e 100644 --- a/src/main/res/values-iw/strings.xml +++ b/src/main/res/values-iw/strings.xml @@ -20,7 +20,6 @@ ממש עכשיו לפני דקה לפני %d דקות - שיחות שלא נקראו שולח... כעת מפענח צופן הודעה. אנא המתן… הודעה מוצפנת OpenPGP diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index ca3750d61..0751ba481 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -28,7 +28,6 @@ ちょうど今 1 分前 %d 分前 - 未読の会話 送信中… メッセージを復号化しています。しばらくお待ちください… OpenPGP 暗号化メッセージ diff --git a/src/main/res/values-ko/strings.xml b/src/main/res/values-ko/strings.xml index cf55f1788..90e516032 100644 --- a/src/main/res/values-ko/strings.xml +++ b/src/main/res/values-ko/strings.xml @@ -21,7 +21,6 @@ 방금 1분 전 %d 분 전 - 읽지 않은 대화 보내는중... 메세지 복호화중입니다. 기다리세요... OpenPGP로 암호화된 메세지 diff --git a/src/main/res/values-nb-rNO/strings.xml b/src/main/res/values-nb-rNO/strings.xml index 005854ab4..9991a5fdc 100644 --- a/src/main/res/values-nb-rNO/strings.xml +++ b/src/main/res/values-nb-rNO/strings.xml @@ -24,7 +24,6 @@ akkurat nå 1 minutt siden %d minutter siden - uleste samtaler sender... Dekrypterer melding mens du venter. OpenPGP-kryptert melding diff --git a/src/main/res/values-nl/strings.xml b/src/main/res/values-nl/strings.xml index b1e74c29b..f39346363 100644 --- a/src/main/res/values-nl/strings.xml +++ b/src/main/res/values-nl/strings.xml @@ -30,7 +30,6 @@ zojuist 1 min. geleden %d min. geleden - ongelezen gesprekken versturen… Bericht aan het ontsleutelen. Even geduld… OpenPGP-versleuteld bericht diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index bb9b85a0d..253161494 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -30,7 +30,6 @@ przed chwilą minutę temu %d minut temu - nieprzeczytanych konwersacji wysyłanie... Odszyfrowywanie wiadomości. To zajmie tylko chwilę... Wiadomość zaszyfrowana OpenPGP diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 1331b8348..4633f0fdf 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -17,6 +17,8 @@ Desbloquear contato Bloquear domínio Desbloquear domínio + Bloquear participante + Desbloquear participante Gerenciar contas Configurações Compartilhar com a conversa @@ -28,7 +30,7 @@ agora 1 minuto atrás %d minutos atrás - conversas não lidas + %d conversas não lidas enviando... Descriptografando a mensagem. Por favor, aguarde... Mensagem criptografada via OpenPGP @@ -861,4 +863,12 @@ Isso se parece com um endereço de domínio Adicionar mesmo assim Isso se parece com um endereço de canal + Compartilhar arquivos de backup + Backup do Conversations + Evento + Abrir backup + O arquivo que você selecionou não é um backup do Conversations + Esta conta já foi configurada + Por favor, digite a senha para esta conta + Não foi possível executar essa ação diff --git a/src/main/res/values-pt/strings.xml b/src/main/res/values-pt/strings.xml index e3f978014..a5f0989ce 100644 --- a/src/main/res/values-pt/strings.xml +++ b/src/main/res/values-pt/strings.xml @@ -23,7 +23,6 @@ agora há pouco 1 minuto atrás %d minutos atrás - Conversas não lidas enviando... Decifrando a mensagem. Por favor aguarde... Mensagem cifrada OpenPGP diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index bcce3ce36..5f1159cbb 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -30,7 +30,6 @@ în acest moment acum un minut acum %d minute - conversații necitite trimitere... Decriptez mesaj. Te rog așteaptă... Mesaj criptat cu OpenPGP diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 18c6c61e5..edae589e7 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -27,7 +27,6 @@ только что 1 минуту назад %d мин. назад - сообщен. не прочитано отправка… Расшифровка сообщения. Подождите… OpenPGP зашифр. сообщение diff --git a/src/main/res/values-sk/strings.xml b/src/main/res/values-sk/strings.xml index 27def940e..6862167ed 100644 --- a/src/main/res/values-sk/strings.xml +++ b/src/main/res/values-sk/strings.xml @@ -20,7 +20,6 @@ práve teraz pred 1 minútou pred %d minútami - neprečítané konverzácie posielam... Prezývka už existuje Administrátor diff --git a/src/main/res/values-sr/strings.xml b/src/main/res/values-sr/strings.xml index 423438a34..0c290fafc 100644 --- a/src/main/res/values-sr/strings.xml +++ b/src/main/res/values-sr/strings.xml @@ -25,7 +25,6 @@ управо сад пре минут пре %d минута - непрочитане преписке шаљем… Дешифрујем поруку, сачекајте… ОпенПГП шифрована порука diff --git a/src/main/res/values-sv/strings.xml b/src/main/res/values-sv/strings.xml index d3fc49773..312d1f180 100644 --- a/src/main/res/values-sv/strings.xml +++ b/src/main/res/values-sv/strings.xml @@ -21,7 +21,6 @@ just nu 1 min sedan %d min sedan - olästa konversationer skickar… Avkrypterar meddelande. Vänta… OpenPGP-krypterat meddelande diff --git a/src/main/res/values-tr-rTR/strings.xml b/src/main/res/values-tr-rTR/strings.xml index b98cfac38..b49385a81 100644 --- a/src/main/res/values-tr-rTR/strings.xml +++ b/src/main/res/values-tr-rTR/strings.xml @@ -21,7 +21,6 @@ şimdi 1 dakika önce %d dakika önc - okunmamış konuşmalar gönderiyor… İleti deşifre ediliyor. Lütfen bekleyin… OpenPGP şifreli ileti diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml index 753da0e64..216a8fc4e 100644 --- a/src/main/res/values-uk/strings.xml +++ b/src/main/res/values-uk/strings.xml @@ -28,7 +28,6 @@ щойно 1 хвилину тому %d хвилин тому - не переглянуті розмови відправляю… Розшифровую повідомлення. Зачекайте, будь ласка… Повідомлення, зашифроване OpenPGP diff --git a/src/main/res/values-vi/strings.xml b/src/main/res/values-vi/strings.xml index c2536a70b..153b8d706 100644 --- a/src/main/res/values-vi/strings.xml +++ b/src/main/res/values-vi/strings.xml @@ -21,7 +21,6 @@ mới đây 1 phút trước %d phút trước - Các hội thoại chưa đọc đang gửi... Đang giải mã tin nhắn. Xin chờ... Tin nhắn mã hoá bằng OpenPGP diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 0bfd598a8..3b4fa89f0 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -28,7 +28,6 @@ 刚刚 1分钟前 %d分钟前 - 未读会话 正在发送… 解密信息中. 请稍候… OpenPGP 加密的信息 diff --git a/src/main/res/values-zh-rTW/strings.xml b/src/main/res/values-zh-rTW/strings.xml index 73a74d27c..3d786abe5 100644 --- a/src/main/res/values-zh-rTW/strings.xml +++ b/src/main/res/values-zh-rTW/strings.xml @@ -21,7 +21,6 @@ 剛剛 1 分鐘前 %d分鐘前 - 未讀會話 正在發送… 訊息解密中,請稍候… OpenPGP 加密的信息 diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 57dd78de4..ebd300dbe 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -30,7 +30,7 @@ just now 1 min ago %d mins ago - unread Conversations + %d unread conversations sending… Decrypting message. Please wait… OpenPGP encrypted message diff --git a/src/quicksy/res/values-pt-rBR/strings.xml b/src/quicksy/res/values-pt-rBR/strings.xml index 6e78e69fd..54502a36e 100644 --- a/src/quicksy/res/values-pt-rBR/strings.xml +++ b/src/quicksy/res/values-pt-rBR/strings.xml @@ -19,4 +19,8 @@ O Quicksy necessita de acesso ao microfone Essa categoria de notificação é utilizada para exibir uma notificação permanente indicando que o Quicksy está em execução. Imagem de perfil do Quicksy - + Quicksy agora está disponível no seu país. + Não foi possível verificar a identidade do servidor. + Erro de segurança desconhecido. + Tempo esgotado ao tentar conectar ao servidor. +