Merge tag '2.5.8' into develop
This commit is contained in:
commit
fa0452761f
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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<Message> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Result> resolve(String domain) {
|
||||
final List<Result> ipResults = fromIpAddress(domain);
|
||||
if (ipResults.size() > 0) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<InetAddress> getLocalAddresses() {
|
||||
final List<InetAddress> addresses = new ArrayList<>();
|
||||
final Enumeration<NetworkInterface> interfaces;
|
||||
try {
|
||||
interfaces = NetworkInterface.getNetworkInterfaces();
|
||||
} catch (SocketException e) {
|
||||
return addresses;
|
||||
}
|
||||
while (interfaces.hasMoreElements()) {
|
||||
NetworkInterface networkInterface = interfaces.nextElement();
|
||||
final Enumeration<InetAddress> 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<JingleCandidate> getLocalCandidates(Jid jid) {
|
||||
SecureRandom random = new SecureRandom();
|
||||
ArrayList<JingleCandidate> 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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");
|
||||
|
|
|
@ -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<String> 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<String> features = getRemoteFeatures();
|
||||
if (features.contains(Content.Version.FT_5.getNamespace())) {
|
||||
private void gatherAndConnectDirectCandidates() {
|
||||
final List<JingleCandidate> 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<String> 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<JingleCandidate> candidates) {
|
||||
Collections.sort(candidates, (a, b) -> Integer.compare(b.getPriority(), a.getPriority()));
|
||||
for (JingleCandidate c : candidates) {
|
||||
mergeCandidate(c);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,6 @@
|
|||
<string name="just_now">الآن</string>
|
||||
<string name="minute_ago">منذ 1 دقيقة</string>
|
||||
<string name="minutes_ago">دقائق %d منذ</string>
|
||||
<string name="unread_conversations">محادثات غير مقروءة</string>
|
||||
<string name="sending">ارسال</string>
|
||||
<string name="message_decrypting">حل شيفرة الرسالة. الرجاء الإنتظار ...</string>
|
||||
<string name="pgp_message">رسالة مشفرة عبر OpenPGP</string>
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
<string name="just_now">току-що</string>
|
||||
<string name="minute_ago">преди 1 минута</string>
|
||||
<string name="minutes_ago">преди %d минути</string>
|
||||
<string name="unread_conversations">непрочетени разговори</string>
|
||||
<string name="sending">изпращане…</string>
|
||||
<string name="message_decrypting">Дешифроване на съобщението. Моля, изчакайте…</string>
|
||||
<string name="pgp_message">Съобщение, шифр. чрез OpenPGP</string>
|
||||
|
|
|
@ -27,7 +27,6 @@
|
|||
<string name="just_now">Ara</string>
|
||||
<string name="minute_ago">fa 1 min</string>
|
||||
<string name="minutes_ago">fa %d mins</string>
|
||||
<string name="unread_conversations">Converses sense llegir o no llegides</string>
|
||||
<string name="sending">enviant…</string>
|
||||
<string name="message_decrypting">Desxifrant el missatge. Espereu…</string>
|
||||
<string name="pgp_message">Missatge xifrat amb OpenPGP</string>
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
<string name="just_now">právě teď</string>
|
||||
<string name="minute_ago">před minutou</string>
|
||||
<string name="minutes_ago">před %d minutami</string>
|
||||
<string name="unread_conversations">nepřečtené konverzace</string>
|
||||
<string name="sending">odesílám…</string>
|
||||
<string name="message_decrypting">Dešifrování zprávy. Chvíli strpení...</string>
|
||||
<string name="pgp_message">OpenPGP šifrovaná zpráva</string>
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<string name="just_now">gerade</string>
|
||||
<string name="minute_ago">vor einer Minute</string>
|
||||
<string name="minutes_ago">vor %d Minuten</string>
|
||||
<string name="unread_conversations">ungelesene Unterhaltungen</string>
|
||||
<string name="x_unread_conversations">%d ungelesene Unterhaltungen</string>
|
||||
<string name="sending">senden…</string>
|
||||
<string name="message_decrypting">Nachricht wird entschlüsselt. Bitte warten …</string>
|
||||
<string name="pgp_message">OpenPGP-verschlüsselte Nachricht</string>
|
||||
|
|
|
@ -30,7 +30,6 @@
|
|||
<string name="just_now">μόλις τώρα</string>
|
||||
<string name="minute_ago">πριν από 1 λεπτό</string>
|
||||
<string name="minutes_ago">πριν από %d λεπτά</string>
|
||||
<string name="unread_conversations">μη αναγνωσμένες Συζητήσεις</string>
|
||||
<string name="sending">αποστολή...</string>
|
||||
<string name="message_decrypting">Αποκρυπτογράφηση μηνύματος. Παρακαλώ περιμένετε...</string>
|
||||
<string name="pgp_message">OpenPGP κρυπτογραφημένο μήνυμα</string>
|
||||
|
|
|
@ -30,7 +30,6 @@
|
|||
<string name="just_now">ahora</string>
|
||||
<string name="minute_ago">hace 1 min</string>
|
||||
<string name="minutes_ago">hace %d min</string>
|
||||
<string name="unread_conversations">conversaciones por leer</string>
|
||||
<string name="sending">enviando…</string>
|
||||
<string name="message_decrypting">Descifrando mensaje. Por favor, espera...</string>
|
||||
<string name="pgp_message">Mensaje cifrado con OpenPGP</string>
|
||||
|
|
|
@ -30,7 +30,6 @@
|
|||
<string name="just_now">orain</string>
|
||||
<string name="minute_ago">min 1 lehenago</string>
|
||||
<string name="minutes_ago">%d min lehenago</string>
|
||||
<string name="unread_conversations">irakurri gabeko elkarrizketak</string>
|
||||
<string name="sending">bidaltzen…</string>
|
||||
<string name="message_decrypting">Mezua desenkriptatzen. Mesedez itxaron…</string>
|
||||
<string name="pgp_message">OpenPGPz enkriptatutako mezua</string>
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
<string name="just_now">هم اکنون</string>
|
||||
<string name="minute_ago">1 دقیقه قبل</string>
|
||||
<string name="minutes_ago">%d دقیقه قبل</string>
|
||||
<string name="unread_conversations">گفتگو های خوانده نشده</string>
|
||||
<string name="sending">در حال ارسال...</string>
|
||||
<string name="message_decrypting">در حال رمزگشایی پیام. لطفا صبور باشید...</string>
|
||||
<string name="pgp_message">پیام رمز شده به وسیله OpenPGP</string>
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
<string name="just_now">À l\'instant</string>
|
||||
<string name="minute_ago">Il y a 1 minute</string>
|
||||
<string name="minutes_ago">Il y a %d minutes</string>
|
||||
<string name="unread_conversations">Conversations non lues</string>
|
||||
<string name="sending">Envoi…</string>
|
||||
<string name="message_decrypting">Déchiffrement du message. Veuillez patienter...</string>
|
||||
<string name="pgp_message">Message chiffré avec OpenPGP</string>
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<string name="just_now">agora</string>
|
||||
<string name="minute_ago">Hai 1 min</string>
|
||||
<string name="minutes_ago">hai %d minutos</string>
|
||||
<string name="unread_conversations">conversas sen ler</string>
|
||||
<string name="x_unread_conversations">%d conversas non lidas</string>
|
||||
<string name="sending">enviando…</string>
|
||||
<string name="message_decrypting">Descifrando a mensaxe. Por favor agarde...</string>
|
||||
<string name="pgp_message">Mensaxe cifrado con OpenPGP</string>
|
||||
|
|
|
@ -30,7 +30,6 @@
|
|||
<string name="just_now">Éppen most</string>
|
||||
<string name="minute_ago">1 perce</string>
|
||||
<string name="minutes_ago">%d perce</string>
|
||||
<string name="unread_conversations">olvasatlan beszélgetés</string>
|
||||
<string name="sending">küldés...</string>
|
||||
<string name="message_decrypting">Üzenet dekódolása. Kérem várjon...</string>
|
||||
<string name="pgp_message">OpenPGP kódolású üzenet</string>
|
||||
|
@ -871,4 +870,5 @@
|
|||
<string name="not_a_backup_file">A kiválasztott fájl nem a Conversations biztonsági mentése</string>
|
||||
<string name="account_already_setup">Ez a fiók már be lett állítva</string>
|
||||
<string name="please_enter_password">Kérem, adja meg a fiókhoz tartozó jelszót</string>
|
||||
<string name="unable_to_perform_this_action">Nem sikerült ezt a cselekvést elvégezni</string>
|
||||
</resources>
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
<string name="just_now">sekarang</string>
|
||||
<string name="minute_ago">1 min lalu</string>
|
||||
<string name="minutes_ago">%d min lalu</string>
|
||||
<string name="unread_conversations">Percakapan belum dibaca</string>
|
||||
<string name="sending">mengirim...</string>
|
||||
<string name="message_decrypting">Mendekripsi pesan. Mohon tunggu…</string>
|
||||
<string name="pgp_message">Pesan terenkripsi OpenPGP</string>
|
||||
|
|
|
@ -30,7 +30,6 @@
|
|||
<string name="just_now">adesso</string>
|
||||
<string name="minute_ago">1 min fa</string>
|
||||
<string name="minutes_ago">%d min fa</string>
|
||||
<string name="unread_conversations">Conversazioni non lette</string>
|
||||
<string name="sending">invio…</string>
|
||||
<string name="message_decrypting">Decifrazione messaggio. Attendere prego...</string>
|
||||
<string name="pgp_message">Messaggio cifrato con OpenPGP</string>
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
<string name="just_now">ממש עכשיו</string>
|
||||
<string name="minute_ago">לפני דקה</string>
|
||||
<string name="minutes_ago">לפני %d דקות</string>
|
||||
<string name="unread_conversations">שיחות שלא נקראו</string>
|
||||
<string name="sending">שולח...</string>
|
||||
<string name="message_decrypting">כעת מפענח צופן הודעה. אנא המתן…</string>
|
||||
<string name="pgp_message">הודעה מוצפנת OpenPGP</string>
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
<string name="just_now">ちょうど今</string>
|
||||
<string name="minute_ago">1 分前</string>
|
||||
<string name="minutes_ago">%d 分前</string>
|
||||
<string name="unread_conversations">未読の会話</string>
|
||||
<string name="sending">送信中…</string>
|
||||
<string name="message_decrypting">メッセージを復号化しています。しばらくお待ちください…</string>
|
||||
<string name="pgp_message">OpenPGP 暗号化メッセージ</string>
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
<string name="just_now">방금 </string>
|
||||
<string name="minute_ago">1분 전 </string>
|
||||
<string name="minutes_ago">%d 분 전 </string>
|
||||
<string name="unread_conversations">읽지 않은 대화 </string>
|
||||
<string name="sending">보내는중... </string>
|
||||
<string name="message_decrypting">메세지 복호화중입니다. 기다리세요...</string>
|
||||
<string name="pgp_message">OpenPGP로 암호화된 메세지</string>
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
<string name="just_now">akkurat nå</string>
|
||||
<string name="minute_ago">1 minutt siden</string>
|
||||
<string name="minutes_ago">%d minutter siden</string>
|
||||
<string name="unread_conversations">uleste samtaler</string>
|
||||
<string name="sending">sender...</string>
|
||||
<string name="message_decrypting">Dekrypterer melding mens du venter.</string>
|
||||
<string name="pgp_message">OpenPGP-kryptert melding</string>
|
||||
|
|
|
@ -30,7 +30,6 @@
|
|||
<string name="just_now">zojuist</string>
|
||||
<string name="minute_ago">1 min. geleden</string>
|
||||
<string name="minutes_ago">%d min. geleden</string>
|
||||
<string name="unread_conversations">ongelezen gesprekken</string>
|
||||
<string name="sending">versturen…</string>
|
||||
<string name="message_decrypting">Bericht aan het ontsleutelen. Even geduld…</string>
|
||||
<string name="pgp_message">OpenPGP-versleuteld bericht</string>
|
||||
|
|
|
@ -30,7 +30,6 @@
|
|||
<string name="just_now">przed chwilą</string>
|
||||
<string name="minute_ago">minutę temu</string>
|
||||
<string name="minutes_ago">%d minut temu</string>
|
||||
<string name="unread_conversations">nieprzeczytanych konwersacji</string>
|
||||
<string name="sending">wysyłanie...</string>
|
||||
<string name="message_decrypting">Odszyfrowywanie wiadomości. To zajmie tylko chwilę...</string>
|
||||
<string name="pgp_message">Wiadomość zaszyfrowana OpenPGP</string>
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
<string name="action_unblock_contact">Desbloquear contato</string>
|
||||
<string name="action_block_domain">Bloquear domínio</string>
|
||||
<string name="action_unblock_domain">Desbloquear domínio</string>
|
||||
<string name="action_block_participant">Bloquear participante</string>
|
||||
<string name="action_unblock_participant">Desbloquear participante</string>
|
||||
<string name="title_activity_manage_accounts">Gerenciar contas</string>
|
||||
<string name="title_activity_settings">Configurações</string>
|
||||
<string name="title_activity_sharewith">Compartilhar com a conversa</string>
|
||||
|
@ -28,7 +30,7 @@
|
|||
<string name="just_now">agora</string>
|
||||
<string name="minute_ago">1 minuto atrás</string>
|
||||
<string name="minutes_ago">%d minutos atrás</string>
|
||||
<string name="unread_conversations">conversas não lidas</string>
|
||||
<string name="x_unread_conversations">%d conversas não lidas</string>
|
||||
<string name="sending">enviando...</string>
|
||||
<string name="message_decrypting">Descriptografando a mensagem. Por favor, aguarde...</string>
|
||||
<string name="pgp_message">Mensagem criptografada via OpenPGP</string>
|
||||
|
@ -861,4 +863,12 @@
|
|||
<string name="this_looks_like_a_domain">Isso se parece com um endereço de domínio</string>
|
||||
<string name="add_anway">Adicionar mesmo assim</string>
|
||||
<string name="this_looks_like_channel">Isso se parece com um endereço de canal</string>
|
||||
<string name="share_backup_files">Compartilhar arquivos de backup</string>
|
||||
<string name="conversations_backup">Backup do Conversations</string>
|
||||
<string name="event">Evento</string>
|
||||
<string name="open_backup">Abrir backup</string>
|
||||
<string name="not_a_backup_file">O arquivo que você selecionou não é um backup do Conversations</string>
|
||||
<string name="account_already_setup">Esta conta já foi configurada</string>
|
||||
<string name="please_enter_password">Por favor, digite a senha para esta conta</string>
|
||||
<string name="unable_to_perform_this_action">Não foi possível executar essa ação</string>
|
||||
</resources>
|
||||
|
|
|
@ -23,7 +23,6 @@
|
|||
<string name="just_now">agora há pouco</string>
|
||||
<string name="minute_ago">1 minuto atrás</string>
|
||||
<string name="minutes_ago">%d minutos atrás</string>
|
||||
<string name="unread_conversations">Conversas não lidas</string>
|
||||
<string name="sending">enviando...</string>
|
||||
<string name="message_decrypting">Decifrando a mensagem. Por favor aguarde...</string>
|
||||
<string name="pgp_message">Mensagem cifrada OpenPGP</string>
|
||||
|
|
|
@ -30,7 +30,6 @@
|
|||
<string name="just_now">în acest moment</string>
|
||||
<string name="minute_ago">acum un minut</string>
|
||||
<string name="minutes_ago">acum %d minute</string>
|
||||
<string name="unread_conversations">conversații necitite</string>
|
||||
<string name="sending">trimitere...</string>
|
||||
<string name="message_decrypting">Decriptez mesaj. Te rog așteaptă...</string>
|
||||
<string name="pgp_message">Mesaj criptat cu OpenPGP</string>
|
||||
|
|
|
@ -27,7 +27,6 @@
|
|||
<string name="just_now">только что</string>
|
||||
<string name="minute_ago">1 минуту назад</string>
|
||||
<string name="minutes_ago">%d мин. назад</string>
|
||||
<string name="unread_conversations">сообщен. не прочитано</string>
|
||||
<string name="sending">отправка…</string>
|
||||
<string name="message_decrypting">Расшифровка сообщения. Подождите…</string>
|
||||
<string name="pgp_message">OpenPGP зашифр. сообщение</string>
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
<string name="just_now">práve teraz</string>
|
||||
<string name="minute_ago">pred 1 minútou</string>
|
||||
<string name="minutes_ago">pred %d minútami</string>
|
||||
<string name="unread_conversations">neprečítané konverzácie</string>
|
||||
<string name="sending">posielam...</string>
|
||||
<string name="nick_in_use">Prezývka už existuje</string>
|
||||
<string name="admin">Administrátor</string>
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
<string name="just_now">управо сад</string>
|
||||
<string name="minute_ago">пре минут</string>
|
||||
<string name="minutes_ago">пре %d минута</string>
|
||||
<string name="unread_conversations">непрочитане преписке</string>
|
||||
<string name="sending">шаљем…</string>
|
||||
<string name="message_decrypting">Дешифрујем поруку, сачекајте…</string>
|
||||
<string name="pgp_message">ОпенПГП шифрована порука</string>
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
<string name="just_now">just nu</string>
|
||||
<string name="minute_ago">1 min sedan</string>
|
||||
<string name="minutes_ago">%d min sedan</string>
|
||||
<string name="unread_conversations">olästa konversationer</string>
|
||||
<string name="sending">skickar…</string>
|
||||
<string name="message_decrypting">Avkrypterar meddelande. Vänta…</string>
|
||||
<string name="pgp_message">OpenPGP-krypterat meddelande</string>
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
<string name="just_now">şimdi</string>
|
||||
<string name="minute_ago">1 dakika önce</string>
|
||||
<string name="minutes_ago">%d dakika önc</string>
|
||||
<string name="unread_conversations">okunmamış konuşmalar</string>
|
||||
<string name="sending">gönderiyor…</string>
|
||||
<string name="message_decrypting">İleti deşifre ediliyor. Lütfen bekleyin…</string>
|
||||
<string name="pgp_message">OpenPGP şifreli ileti</string>
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
<string name="just_now">щойно</string>
|
||||
<string name="minute_ago">1 хвилину тому</string>
|
||||
<string name="minutes_ago">%d хвилин тому</string>
|
||||
<string name="unread_conversations">не переглянуті розмови </string>
|
||||
<string name="sending">відправляю…</string>
|
||||
<string name="message_decrypting">Розшифровую повідомлення. Зачекайте, будь ласка…</string>
|
||||
<string name="pgp_message">Повідомлення, зашифроване OpenPGP</string>
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
<string name="just_now">mới đây</string>
|
||||
<string name="minute_ago">1 phút trước</string>
|
||||
<string name="minutes_ago">%d phút trước</string>
|
||||
<string name="unread_conversations">Các hội thoại chưa đọc</string>
|
||||
<string name="sending">đang gửi...</string>
|
||||
<string name="message_decrypting">Đang giải mã tin nhắn. Xin chờ...</string>
|
||||
<string name="pgp_message">Tin nhắn mã hoá bằng OpenPGP</string>
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
<string name="just_now">刚刚</string>
|
||||
<string name="minute_ago">1分钟前</string>
|
||||
<string name="minutes_ago">%d分钟前</string>
|
||||
<string name="unread_conversations">未读会话</string>
|
||||
<string name="sending">正在发送…</string>
|
||||
<string name="message_decrypting">解密信息中. 请稍候…</string>
|
||||
<string name="pgp_message">OpenPGP 加密的信息</string>
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
<string name="just_now">剛剛</string>
|
||||
<string name="minute_ago">1 分鐘前</string>
|
||||
<string name="minutes_ago">%d分鐘前</string>
|
||||
<string name="unread_conversations">未讀會話</string>
|
||||
<string name="sending">正在發送…</string>
|
||||
<string name="message_decrypting">訊息解密中,請稍候…</string>
|
||||
<string name="pgp_message">OpenPGP 加密的信息</string>
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<string name="just_now">just now</string>
|
||||
<string name="minute_ago">1 min ago</string>
|
||||
<string name="minutes_ago">%d mins ago</string>
|
||||
<string name="unread_conversations">unread Conversations</string>
|
||||
<string name="x_unread_conversations">%d unread conversations</string>
|
||||
<string name="sending">sending…</string>
|
||||
<string name="message_decrypting">Decrypting message. Please wait…</string>
|
||||
<string name="pgp_message">OpenPGP encrypted message</string>
|
||||
|
|
|
@ -19,4 +19,8 @@
|
|||
<string name="no_microphone_permission">O Quicksy necessita de acesso ao microfone</string>
|
||||
<string name="foreground_service_channel_description">Essa categoria de notificação é utilizada para exibir uma notificação permanente indicando que o Quicksy está em execução.</string>
|
||||
<string name="set_profile_picture">Imagem de perfil do Quicksy</string>
|
||||
</resources>
|
||||
<string name="not_available_in_your_country">Quicksy agora está disponível no seu país.</string>
|
||||
<string name="unable_to_verify_server_identity">Não foi possível verificar a identidade do servidor.</string>
|
||||
<string name="unknown_security_error">Erro de segurança desconhecido.</string>
|
||||
<string name="timeout_while_connecting_to_server">Tempo esgotado ao tentar conectar ao servidor.</string>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in New Issue