/*
* Kontalk Android client
* Copyright (C) 2017 Kontalk Devteam <devteam@kontalk.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kontalk.client;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import org.apache.http.conn.ssl.AllowAllHostnameVerifier;
import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode;
import org.jivesoftware.smack.SASLAuthentication;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.StanzaFilter;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.sm.predicates.ForMatchingPredicateOrAfterXStanzas;
import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
import org.jivesoftware.smackx.receipts.DeliveryReceipt;
import org.jivesoftware.smackx.receipts.DeliveryReceiptRequest;
import android.annotation.SuppressLint;
import org.kontalk.BuildConfig;
import org.kontalk.Kontalk;
import org.kontalk.Log;
import org.kontalk.authenticator.LegacyAuthentication;
public class KontalkConnection extends XMPPTCPConnection {
private static final String TAG = Kontalk.TAG;
/** Packet reply timeout. */
public static final int DEFAULT_PACKET_TIMEOUT = 15000;
protected EndpointServer mServer;
public KontalkConnection(String resource, EndpointServer server, boolean secure,
boolean acceptAnyCertificate, KeyStore trustStore, String legacyAuthToken)
throws XMPPException {
this(resource, server, secure, null, null, acceptAnyCertificate, trustStore, legacyAuthToken);
}
public KontalkConnection(String resource, EndpointServer server, boolean secure,
PrivateKey privateKey, X509Certificate bridgeCert,
boolean acceptAnyCertificate, KeyStore trustStore, String legacyAuthToken) throws XMPPException {
super(buildConfiguration(resource, server, secure,
privateKey, bridgeCert, acceptAnyCertificate, trustStore, legacyAuthToken));
mServer = server;
// enable SM without resumption
setUseStreamManagement(true);
setUseStreamManagementResumption(false);
// set custom ack predicate
addRequestAckPredicate(AckPredicate.INSTANCE);
// set custom packet reply timeout
setPacketReplyTimeout(DEFAULT_PACKET_TIMEOUT);
}
private static XMPPTCPConnectionConfiguration buildConfiguration(String resource,
EndpointServer server, boolean secure, PrivateKey privateKey, X509Certificate bridgeCert,
boolean acceptAnyCertificate, KeyStore trustStore, String legacyAuthToken) {
XMPPTCPConnectionConfiguration.Builder builder =
XMPPTCPConnectionConfiguration.builder();
builder
// connection parameters
.setHost(server.getHost())
.setPort(secure ? server.getSecurePort() : server.getPort())
.setServiceName(server.getNetwork())
.setResource(resource)
// the dummy value is not actually used
.setUsernameAndPassword(null, legacyAuthToken != null ? legacyAuthToken : "dummy")
// for EXTERNAL
.allowEmptyOrNullUsernames()
// enable compression
.setCompressionEnabled(true)
// enable encryption
.setSecurityMode(secure ? SecurityMode.disabled : SecurityMode.required)
// we will send a custom presence
.setSendPresence(false)
// disable session initiation
.setLegacySessionDisabled(true)
// enable debugging
.setDebuggerEnabled(BuildConfig.DEBUG);
// setup SSL
setupSSL(builder, secure, privateKey, bridgeCert, acceptAnyCertificate, trustStore);
return builder.build();
}
@SuppressLint("AllowAllHostnameVerifier")
private static void setupSSL(XMPPTCPConnectionConfiguration.Builder builder,
boolean direct, PrivateKey privateKey, X509Certificate bridgeCert,
boolean acceptAnyCertificate, KeyStore trustStore) {
try {
SSLContext ctx = SSLContext.getInstance("TLS");
KeyManager[] km = null;
if (privateKey != null && bridgeCert != null) {
// in-memory keystore
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
keystore.load(null, null);
keystore.setKeyEntry("private", privateKey, null, new Certificate[] { bridgeCert });
// key managers
KeyManagerFactory kmFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmFactory.init(keystore, null);
km = kmFactory.getKeyManagers();
// disable PLAIN mechanism if not upgrading from legacy
if (!LegacyAuthentication.isUpgrading()) {
// blacklist PLAIN mechanism
SASLAuthentication.blacklistSASLMechanism("PLAIN");
}
}
// trust managers
TrustManager[] tm;
if (acceptAnyCertificate) {
tm = new TrustManager[] {
new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
@SuppressLint("TrustAllX509TrustManager")
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
}
@SuppressLint("TrustAllX509TrustManager")
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
}
}
};
builder.setHostnameVerifier(new AllowAllHostnameVerifier());
}
else {
// builtin keystore
TrustManagerFactory tmFactory = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmFactory.init(trustStore);
tm = tmFactory.getTrustManagers();
}
ctx.init(km, tm, null);
builder.setCustomSSLContext(ctx);
if (direct)
builder.setSocketFactory(ctx.getSocketFactory());
// SASL EXTERNAL is already enabled in Smack
}
catch (Exception e) {
Log.w(TAG, "unable to setup SSL connection", e);
}
}
@Override
protected void processPacket(Stanza packet) throws InterruptedException {
if (packet instanceof Message) {
/*
* We are receiving a message. Suspend SM ack replies because we
* want to wait for our message listener to be invoked and have time
* to store the message to the database.
*/
suspendSmAck();
}
super.processPacket(packet);
}
/**
* A custom ack predicate that allows ack after a message with a delivery
* receipt, a receipt request or a body, or after 5 stanzas.
*/
private static final class AckPredicate extends ForMatchingPredicateOrAfterXStanzas {
public static final AckPredicate INSTANCE = new AckPredicate();
private AckPredicate() {
super(new StanzaFilter() {
@Override
public boolean accept(Stanza packet) {
return (packet instanceof Message &&
(((Message) packet).getBody() != null ||
DeliveryReceipt.from((Message) packet) != null ||
DeliveryReceiptRequest.from(packet) != null));
}
}, 5);
}
}
}