/*
Copyright (C) 2014-2015, Silent Circle, LLC. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Any redistribution, use, or modification is done solely for personal
benefit and not for any commercial purpose or for monetary gain
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name Silent Circle nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL SILENT CIRCLE, LLC BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.silentcircle.silenttext.client;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import javax.net.SocketFactory;
import org.twuni.xmppt.xml.XMLBuilder;
import org.twuni.xmppt.xml.XMLElement;
import org.twuni.xmppt.xmpp.XMPPClientConnection;
import org.twuni.xmppt.xmpp.XMPPClientConnection.AcknowledgmentListener;
import org.twuni.xmppt.xmpp.XMPPClientConnection.PacketListener;
import org.twuni.xmppt.xmpp.XMPPClientConnectionManager;
import org.twuni.xmppt.xmpp.core.IQ;
import org.twuni.xmppt.xmpp.core.Message;
import org.twuni.xmppt.xmpp.stream.AcknowledgmentRequest;
import org.twuni.xmppt.xmpp.stream.StreamError;
import org.twuni.xmppt.xmpp.stream.StreamManagement;
import android.content.Context;
import com.silentcircle.http.client.exception.NetworkException;
import com.silentcircle.silenttext.application.SilentTextApplication;
import com.silentcircle.silenttext.listener.TransportListener;
import com.silentcircle.silenttext.log.Log;
import com.silentcircle.silenttext.model.Credential;
import com.silentcircle.silenttext.task.GetDeviceChangedTask;
import com.silentcircle.silenttext.transport.Envelope;
import com.silentcircle.silenttext.transport.TransportQueue;
import com.silentcircle.silenttext.transport.TransportQueue.Processor;
import com.silentcircle.silenttext.util.AsyncUtils;
import com.silentcircle.silenttext.util.StringUtils;
public class XMPPTransport implements Processor, AcknowledgmentListener, PacketListener, AcknowledgmentRequestor {
private static class Endpoint {
public final CharSequence host;
public final int port;
public final String serviceName;
public Endpoint( CharSequence host, int port, String serviceName ) {
this.host = host;
this.port = port;
this.serviceName = serviceName;
}
}
public static interface OnDisconnectedListener {
public void onDisconnected( XMPPTransport client );
}
private static class PingTask extends TimerTask {
private final AcknowledgmentRequestor requestor;
public PingTask( AcknowledgmentRequestor requestor ) {
this.requestor = requestor;
}
@Override
public void run() {
requestor.requestAcknowledgment();
}
}
private static class ProcessQueueTask extends TimerTask {
private final TransportQueue queue;
private final Processor processor;
public ProcessQueueTask( TransportQueue queue, Processor processor ) {
this.queue = queue;
this.processor = processor;
}
/**
* @param exception
*/
protected void onException( Throwable exception ) {
// By default, do nothing.
}
@Override
public void run() {
try {
queue.process( processor );
} catch( Throwable exception ) {
onException( exception );
}
}
}
public static final int DEFAULT_PING_INTERVAL = 5 * 60 * 1000;
public static final int DEFAULT_QUEUE_PROCESSOR_INTERVAL = 2500;
public static final int MAXIMUM_CONSECUTIVE_FAILED_ACKNOWLEDGMENTS = 2;
final Context context;
protected final List<TransportListener> listeners = new ArrayList<TransportListener>();
private Log log;
private final String connectionID;
private final XMPPClientConnectionManager connectionManager = new XMPPClientConnectionManager();
private final Endpoint endpoint;
private final TransportQueue queue;
private final Timer timer;
private TimerTask pingTask;
private TimerTask processQueueTask;
private final List<Object> unacknowledgedPackets = new ArrayList<Object>();
private int consecutiveFailedAcknowledgments = 0;
private static final IllegalStateException NOT_CONNECTED = new IllegalStateException( new NetworkException() );
public XMPPTransport( Context context, Credential credential, String resourceName, CharSequence host, int port, TransportQueue queue, SocketFactory socketFactory ) {
this.context = context;
endpoint = new Endpoint( host, port, credential.getDomain() );
connectionID = credential.getUsername();
this.queue = queue;
timer = new Timer( true );
XMPPClientConnection.Builder connection = new XMPPClientConnection.Builder();
connection.logger( new AndroidLogger( "JabberClient" ) );
connection.host( String.valueOf( host ) ).port( port ).secure( true );
connection.serviceName( credential.getDomain() );
connection.userName( credential.getUsername().replaceAll( "^(.+)@(.+)$", "$1" ) ).password( credential.getPassword() );
connection.resourceName( resourceName );
if( socketFactory instanceof XMPPSocketFactory ) {
connection.socketFactory( ( (XMPPSocketFactory) socketFactory ).proxy() );
}
connection.acknowledgmentListener( this );
connection.packetListener( this );
connectionManager.startManaging( connectionID, connection );
}
public void addListener( TransportListener listener ) {
listeners.add( listener );
}
public void adviseReconnect() {
try {
connectionManager.connectAll();
startQueueProcessor();
startPing();
} catch( IllegalStateException exception ) {
throw new NetworkException( exception );
} catch( IOException exception ) {
throw new NetworkException( exception );
}
}
public void disconnect() {
softDisconnect();
connectionManager.stopManaging( connectionID );
}
private XMPPClientConnection getConnection() {
return connectionManager.getConnection( connectionID );
}
public Log getLog() {
if( log == null ) {
log = new Log( "JabberClient" );
}
return log;
}
public CharSequence getServerHost() {
return endpoint.host;
}
public int getServerPort() {
return endpoint.port;
}
public boolean isConnected() {
return connectionManager.isConnected( connectionID );
}
public void onDestroy() {
getLog().onDestroy();
}
@Override
public void onException( XMPPClientConnection connection, Throwable exception ) {
getLog().error( exception, "#onException" );
}
@Override
public void onFailedAcknowledgment( XMPPClientConnection connection, int expected, int actual ) {
getLog().info( "#onFailedAcknowledgment expected:%d actual:%d", Integer.valueOf( expected ), Integer.valueOf( actual ) );
consecutiveFailedAcknowledgments++;
if( consecutiveFailedAcknowledgments >= MAXIMUM_CONSECUTIVE_FAILED_ACKNOWLEDGMENTS ) {
softDisconnect();
try {
adviseReconnect();
} catch( NetworkException exception ) {
getLog().info( exception, "#onFailedAcknowledgment" );
} catch( RuntimeException exception ) {
getLog().info( exception, "#onFailedAcknowledgment" );
}
return;
}
for( Object packet : unacknowledgedPackets ) {
send( packet );
}
if( queue instanceof AcknowledgmentListener ) {
( (AcknowledgmentListener) queue ).onFailedAcknowledgment( connection, expected, actual );
}
}
@Override
public void onPacketReceived( XMPPClientConnection connection, Object packet ) {
if( packet instanceof Message ) {
Message message = (Message) packet;
Envelope envelope = new Envelope();
envelope.time = System.currentTimeMillis();
envelope.id = message.id();
envelope.to = message.to();
envelope.from = message.from();
String content = String.valueOf( message.getContent() );
envelope.content = StringUtils.find( content, "^.*<x.+>(\\?SCIMP:[^\\.]+.)</x>.*$", 1 );
// TODO: look at namespace for special indicator and add it to a new flag on the
// Envelope:
// <body/><x xmlns="http://silentcircle.com/protocol/scimp#public-key">
if( Message.TYPE_ERROR.equals( message.type() ) ) {
String from = envelope.to;
envelope.to = envelope.from;
envelope.from = from;
for( TransportListener listener : listeners ) {
listener.onMessageReturned( envelope, "returned" );
}
} else {
boolean secure = envelope.content != null;
if( !secure ) {
envelope.content = StringUtils.find( content, "^.*<body>(.+)</body>.*$", 1 );
}
for( TransportListener listener : listeners ) {
if( secure ) {
listener.onSecureMessageReceived( envelope );
} else if( envelope.content != null ) {
listener.onInsecureMessageReceived( envelope );
}
}
}
} else if( packet instanceof StreamError ) {
String foundReplacedConnection = StringUtils.find( String.valueOf( packet ).toString(), "^.*(Replaced by new connection).*$", 1 );
if( foundReplacedConnection != null ) {
getLog().info( "#StreamError %s", packet );
AsyncUtils.execute( new GetDeviceChangedTask( context ) {
@Override
protected void onPostExecute( Void result ) {
if( deviceChanged ) {
( (SilentTextApplication) context ).deactivate();
}
}
}, SilentTextApplication.from( context ).getUsername() );
}
} else {
getLog().info( "#onPacketReceived %s", packet );
}
}
@Override
public void onSuccessfulAcknowledgment( XMPPClientConnection connection ) {
getLog().info( "#onSuccessfulAcknowledgment" );
consecutiveFailedAcknowledgments = 0;
unacknowledgedPackets.clear();
if( queue instanceof AcknowledgmentListener ) {
( (AcknowledgmentListener) queue ).onSuccessfulAcknowledgment( connection );
}
}
@Override
public void process( Envelope envelope ) {
if( !connectionManager.isConnected( connectionID ) ) {
throw NOT_CONNECTED;
}
// add an empty <body/> tag for XMPP
XMLBuilder body = new XMLBuilder( "body" );
body.content( "" );
XMLBuilder x = new XMLBuilder( "x" );
x.attribute( XMLElement.ATTRIBUTE_NAMESPACE, "http://silentcircle.com" );
x.attribute( "badge", Boolean.toString( envelope.badgeworthy ) );
x.attribute( "notifiable", Boolean.toString( envelope.notifiable ) );
x.content( envelope.content );
Message message = new Message( envelope.id, Message.TYPE_CHAT, null, envelope.to, body.toString() + x.toString() );
send( message );
for( TransportListener listener : listeners ) {
listener.onMessageSent( envelope );
}
}
public void removeOfflineMessage( String remoteUserID, String messageID ) {
XMLBuilder offline = new XMLBuilder( "offline" );
offline.attribute( XMLElement.ATTRIBUTE_NAMESPACE, "http://silentcircle.com/protocol/offline" );
XMLBuilder item = new XMLBuilder( "item" );
item.attribute( "action", "remove" );
item.attribute( "to", remoteUserID );
item.attribute( "id", messageID );
offline.content( item.close() );
Object iq = new IQ( UUID.randomUUID().toString(), IQ.TYPE_SET, null, endpoint.serviceName, offline );
unacknowledgedPackets.add( iq );
send( iq );
}
@Override
public void requestAcknowledgment() {
if( isConnected() ) {
if( getConnection().isFeatureAvailable( StreamManagement.class ) ) {
send( new AcknowledgmentRequest() );
} else {
onSuccessfulAcknowledgment( getConnection() );
}
}
}
private void send( Object... packets ) {
try {
XMPPClientConnection conn = getConnection();
if( conn != null ) {
conn.send( packets );
}
} catch( IOException exception ) {
getLog().error( exception, "#send" );
}
}
public void sendEncryptedMessage( Envelope envelope ) {
queue.add( envelope );
}
public void setLog( Log log ) {
this.log = log;
}
public void softDisconnect() {
XMPPClientConnection connection = connectionManager.getConnection( connectionID );
if( connection != null ) {
try {
connection.logout();
connection.disconnect();
} catch( IOException exception ) {
getLog().info( exception, "#softDisconnect" );
}
}
stopQueueProcessor();
stopPing();
}
private void startPing() {
startPing( DEFAULT_PING_INTERVAL );
}
private void startPing( int interval ) {
stopPing();
if( isConnected() ) {
pingTask = new PingTask( this );
timer.schedule( pingTask, interval, interval );
}
}
private void startQueueProcessor() {
startQueueProcessor( DEFAULT_QUEUE_PROCESSOR_INTERVAL );
}
private void startQueueProcessor( int interval ) {
stopQueueProcessor();
if( isConnected() ) {
if( queue instanceof AcknowledgmentListener ) {
( (AcknowledgmentListener) queue ).onFailedAcknowledgment( getConnection(), 0, 0 );
}
processQueueTask = new ProcessQueueTask( queue, this ) {
@Override
protected void onException( Throwable exception ) {
getLog().warn( exception, "ProcessQueueTask#onException" );
softDisconnect();
}
};
timer.schedule( processQueueTask, interval, interval );
}
}
private void stopPing() {
if( pingTask != null ) {
pingTask.cancel();
pingTask = null;
}
timer.purge();
}
private void stopQueueProcessor() {
if( processQueueTask != null ) {
processQueueTask.cancel();
processQueueTask = null;
}
timer.purge();
}
}