Java tutorial
/* * 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); } } }