package org.bitseal.network;
import java.util.ArrayList;
import org.bitseal.core.App;
import org.bitseal.core.PubkeyProcessor;
import org.bitseal.data.Payload;
import org.bitseal.data.Pubkey;
import org.bitseal.database.PayloadProvider;
import org.bitseal.database.PayloadsTable;
import org.bitseal.database.PubkeyProvider;
import org.bitseal.database.PubkeysTable;
import org.bitseal.util.ByteFormatter;
import org.bitseal.util.TimeUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Base64;
import android.util.Log;
/**
* Provides various methods used for communication between the
* Bitseal client and Bitseal servers.
*
* @author Jonathan Coe
*/
public class ServerCommunicator
{
/**
* Determines the level of redundancy the client will attempt to maintain when
* attempting to disseminate msgs to the rest of the Bitmessage network. This
* redundancy is intended to provide some rudimentary defence against problems
* caused by malicious or unreliable servers, e.g. a server reporting that it
* has disseminated a msg to the rest of the network but failing to actually do so.
* <br><br>
* Since Bitmessage is built around a flooding mechanism for data transmission,
* this should not create any problems from duplication(e.g. a recipient getting
* the same message multiple times).
*/
private static final int MSG_DISSEMINATION_REDUNDANCY_FACTOR = 2;
/**
* Determines the level of redundancy the client will attempt to maintain when
* attempting to disseminate pubkeys to the rest of the Bitmessage network.
*/
private static final int PUBKEY_DISSEMINATION_REDUNDANCY_FACTOR = 2;
/**
* Determines the level of redundancy the client will attempt to maintain when
* attempting to disseminate getpubkeys to the rest of the Bitmessage network.
*/
private static final int GETPUBKEY_DISSEMINATION_REDUNDANCY_FACTOR = 2;
/**
* The modifier that we use to calculate the 'received since' time
* that we supply to the API the first time we check for msgs sent to a particular
* address. This is currently set to be equal to PyBitmessage's "lengthOfTimeToLeaveObjectsInInventory"
* value, which is currently 2.8 days or 237600 seconds.
*/
private static final long FIRST_CHECK_RECEIVED_TIME_MODIFIER = 237600;
/**
* The default modifier that we used to calculate the 'received since' time
* that we supply to the API when we check for msgs and getpubkeys. This
* allows a little overlap so that msgs which the server received
* very close to the last time we checked for msgs should not be missed.
*/
private static final long DEFAULT_RECEIVED_TIME_MODIFIER = 10;
/**
* The maximum period in seconds which for which we will check for new msgs in a single request.
* This prevents us from being overwhelmed by a huge number of new msgs when we try
* to catch up with the network after some time offline.
*/
private static final long MAXIMUM_MSG_CATCH_UP_PERIOD = 1800;
/**
* The maximum number of servers to poll on each attempt to retrieve data. If we have
* a large number of servers it would take too long to poll all of them on each
* attempt to retrieve data.
*/
private static final int MAX_SERVERS_TO_POLL = 1;
/**
* The maximum size of an incoming payload that we will accept, in bytes.
**/
private static final long MAX_PAYLOAD_SIZE_TO_ACCEPT = 256000;
/** A key used to store the time of the last successful 'check for new msgs' server request */
private static final String LAST_MSG_CHECK_TIME = "lastMsgCheckTime";
// API commands recognised by PyBitmessage
private static final String API_METHOD_DISSEMINATE_MSG = "disseminateMsg";
private static final String API_METHOD_DISSEMINATE_MSG_NO_POW = "disseminateMsgNoPOW";
private static final String API_METHOD_DISSEMINATE_PUBKEY = "disseminatePubkey";
private static final String API_METHOD_DISSEMINATE_PUBKEY_NO_POW = "disseminatePubkeyNoPOW";
private static final String API_METHOD_DISSEMINATE_GETPUBKEY = "disseminateGetpubkey";
private static final String API_METHOD_REQUEST_PUBKEY = "requestPubkey";
private static final String API_METHOD_CHECK_FOR_NEW_MSGS = "checkForNewMsgs";
// Strings returned by the PyBitmessage API which indicate the result of an API call
private static final String RESULT_CODE_DISSEMINATE_MSG = "Message disseminated successfully";
private static final String RESULT_CODE_DISSEMINATE_PUBKEY = "Pubkey disseminated successfully";
private static final String RESULT_CODE_DISSEMINATE_GETPUBKEY = "Getpubkey disseminated successfully";
private static final String RESULT_CODE_REQUEST_PUBKEY = "No pubkeys found";
private static final String RESULT_CODE_CHECK_FOR_NEW_MSGS = "No msgs found";
// Strings used in the JSON formatting of data returned by servers
private static final String JSON_NAME_PUBKEY_PAYLOAD = "pubkeyPayload";
private static final String JSON_NAME_MSG_PAYLOADS = "msgPayloads";
private static final String JSON_NAME_DATA = "data";
private static final String TAG = "SERVER_COMMUNICATOR";
/**
* Attempts to disseminate a message to the rest of the Bitmessage
* network by sending it to one or more Bitseal servers. The proof of work for
* the message has already been done.
*
* @param msgPayload - A byte[] containing the message to be disseminated.
*
* @return A boolean indicating whether or not the dissemination attempt was successful.
*/
public boolean disseminateMsg(byte[] msgPayload)
{
String hexPayload = ByteFormatter.byteArrayToHexString(msgPayload);
Log.d(TAG, "Attempting to disseminate an encrypted msg with POW done.\n"
+ "Encrypted msg payload: " + hexPayload);
// Attempt to make the API call
ApiCaller caller = new ApiCaller();
int successfulCalls = 0;
for(int i = 0; i < MSG_DISSEMINATION_REDUNDANCY_FACTOR; i++)
{
Object callResult = caller.call(API_METHOD_DISSEMINATE_MSG, hexPayload);
String resultString = callResult.toString();
Log.d(TAG, "The result of the 'disseminate msg' API call was: " + resultString);
if (resultString.equals(RESULT_CODE_DISSEMINATE_MSG))
{
successfulCalls = successfulCalls + 1;
}
else
{
Log.e(TAG, "While running disseminateMsgWithPOW(), a server connection was established \n" +
"successfully, but the API call failed. The result of the api call was: " + resultString);
}
try
{
caller.switchToNextServer();
}
catch (RuntimeException e)
{
break;
}
}
// If we successfully disseminated the payload to at least one server, return true
if (successfulCalls > 0)
{
return true;
}
else
{
return false;
}
}
/**
* Attempts to disseminate a message to the rest of the Bitmessage
* network by sending it to one or more Bitseal servers. The proof of work for
* the message has NOT yet been done, so the server will do the proof
* of work and then disseminate the message.
*
* @param msgPayload - A byte[] containing the message to be disseminated.
* @param nonceTrialsPerByte - An int representing the nonceTrialsPerByte value for
* the proof of work required by the destination address
* @param extraBytes - An int representing the extraBytes value for
* the proof of work required by the destination address
*
* @return A boolean indicating whether or not the dissemination attempt was successful.
*/
public boolean disseminateMsgNoPOW(byte[] msgPayload, int nonceTrialsPerByte, int extraBytes)
{
String hexPayload = ByteFormatter.byteArrayToHexString(msgPayload);
Log.d(TAG, "Attempting to disseminate an encrypted msg without POW done.\n"
+ "Encrypted msg payload: " + hexPayload);
// Attempt to make the API call
ApiCaller caller = new ApiCaller();
int successfulCalls = 0;
for(int i = 0; i < MSG_DISSEMINATION_REDUNDANCY_FACTOR; i++)
{
Object callResult = caller.call(API_METHOD_DISSEMINATE_MSG_NO_POW, hexPayload);
String resultString = callResult.toString();
Log.d(TAG, "The result of the 'disseminate msg' API call was: " + resultString);
if (resultString.equals(RESULT_CODE_DISSEMINATE_MSG))
{
successfulCalls = successfulCalls + 1;
}
else
{
Log.e(TAG, "While running disseminateMsgNoPOW(), a server connection was established \n" +
"successfully, but the API call failed. The result of the api call was: " + resultString);
}
try
{
caller.switchToNextServer();
}
catch (RuntimeException e)
{
break;
}
}
// If we successfully disseminated the payload to at least one server, return true
if (successfulCalls > 0)
{
return true;
}
else
{
return false;
}
}
/**
* Attempts to disseminate a pubkey to the rest of the Bitmessage network
* by sending it to one or more Bitseal servers. The proof of work for
* the pubkey has already been done.
*
* @param pubkeyPayload -A byte[] containing the pubkey to be disseminated.
*
* @return A boolean indicating whether or not the dissemination attempt was successful.
*/
public boolean disseminatePubkey(byte[] pubkeyPayload)
{
String hexPayload = ByteFormatter.byteArrayToHexString(pubkeyPayload);
Log.d(TAG, "Attempting to disseminate a pubkey with POW done.\n"
+ "Pubkey payload: " + hexPayload);
// Attempt to make the API call
ApiCaller caller = new ApiCaller();
int successfulCalls = 0;
for(int i = 0; i < PUBKEY_DISSEMINATION_REDUNDANCY_FACTOR; i++)
{
Object callResult = caller.call(API_METHOD_DISSEMINATE_PUBKEY, hexPayload);
String resultString = callResult.toString();
Log.d(TAG, "The result of the 'disseminate pubkey' API call was: " + resultString);
if (resultString.equals(RESULT_CODE_DISSEMINATE_PUBKEY))
{
successfulCalls = successfulCalls + 1;
}
else
{
Log.e(TAG, "While running disseminatePubkeyWithPOW(), a server connection was established \n" +
"successfully, but the API call failed. The result of the api call was: " + resultString);
}
try
{
caller.switchToNextServer();
}
catch (RuntimeException e)
{
break;
}
}
// If we successfully disseminated the payload to at least one server, return true
if (successfulCalls > 0)
{
return true;
}
else
{
return false;
}
}
/**
* Attempts to disseminate a pubkey to the rest of the Bitmessage network
* by sending it to one or more Bitseal servers. The proof of work for
* the pubkey has NOT yet been done, so the server will do the proof
* of work and then disseminate the pubkey.
*
* @param pubkeyPayload -A byte[] containing the pubkey to be disseminated.
*
* @return A boolean indicating whether or not the dissemination attempt was successful.
*/
public boolean disseminatePubkeyNoPOW(byte[] pubkeyPayload)
{
String hexPayload = ByteFormatter.byteArrayToHexString(pubkeyPayload);
Log.d(TAG, "Attempting to disseminate a pubkey without POW done.\n"
+ "Pubkey payload: " + hexPayload);
// Attempt to make the API call
ApiCaller caller = new ApiCaller();
int successfulCalls = 0;
for(int i = 0; i < PUBKEY_DISSEMINATION_REDUNDANCY_FACTOR; i++)
{
Object callResult = caller.call(API_METHOD_DISSEMINATE_PUBKEY_NO_POW, hexPayload);
String resultString = callResult.toString();
Log.d(TAG, "The result of the 'disseminate pubkey' API call was: " + resultString);
if (resultString.equals(RESULT_CODE_DISSEMINATE_PUBKEY))
{
successfulCalls = successfulCalls + 1;
}
else
{
Log.e(TAG, "While running disseminatePubkeyNoPOW(), a server connection was established \n" +
"successfully, but the API call failed. The result of the api call was: " + resultString);
}
try
{
caller.switchToNextServer();
}
catch (RuntimeException e)
{
break;
}
}
// If we successfully disseminated the payload to at least one server, return true
if (successfulCalls > 0)
{
return true;
}
else
{
return false;
}
}
/**
* Sends a getpubkey object to a server, waits some time for the server
* to receive the pubkey specified by the getpubkey object, then requests
* that pubkey from the server.
*
* @param getpubkeyPayload - A byte[] containing the payload of the getpubkey object
*
* @return - A Pubkey object containing the pubkey specified by the
* getpubkey payload
*/
public boolean disseminateGetpubkey(byte[] getpubkeyPayload)
{
String hexPayload = ByteFormatter.byteArrayToHexString(getpubkeyPayload);
Log.d(TAG, "Attempting to disseminate a getpubkey with POW done.\n"
+ "Getpubkey payload: " + hexPayload);
// Attempt to make the API call
ApiCaller caller = new ApiCaller();
int successfulCalls = 0;
for(int i = 0; i < GETPUBKEY_DISSEMINATION_REDUNDANCY_FACTOR; i++)
{
Object callResult = caller.call(API_METHOD_DISSEMINATE_GETPUBKEY, hexPayload);
String resultString = callResult.toString();
Log.d(TAG, "The result of the 'disseminate getpubkey' API call was: " + resultString);
if (resultString.equals(RESULT_CODE_DISSEMINATE_GETPUBKEY))
{
successfulCalls = successfulCalls + 1;
}
else
{
Log.e(TAG, "While running disseminateGetpubkeyWithPOW(), a server connection was established \n" +
"successfully, but the API call failed. The result of the api call was: " + resultString);
}
try
{
caller.switchToNextServer();
}
catch (RuntimeException e)
{
break;
}
}
// If we successfully disseminated the payload to at least one server, return true
if (successfulCalls > 0)
{
return true;
}
else
{
return false;
}
}
/**
* Takes an identifier (ripe hash or tag) and requests the corresponding pubkey from a
* Bitseal server.<br><br>
*
* Note: If the pubkey cannot be retrieved after trying all available servers,
* this method will throw a RuntimeException
*
* @param addressString - A String containing the address which we are trying
* to retrieve the pubkey of
* @param identifier - A byte[] containing the data used to identify the pubkey
* we wish to request. For address versions 3 and below, this is the ripe hash. For
* address versions 4 and above, this is the 'tag'.
* @param addressVersion - An int containing the version number of the address for
* which we are requesting the pubkey
*
* @return A Pubkey object containing the requested pubkey
*/
public Pubkey requestPubkeyFromServer(String addressString, byte[] identifier, int addressVersion)
{
Log.d(TAG, "Requesting the pubkey of address " + addressString);
// Encode the payload to be sent into hex
String hexPayload = ByteFormatter.byteArrayToHexString(identifier);
// Work out how many servers to poll in this request
ApiCaller caller = new ApiCaller();
int serversToPoll = caller.getNumberOfServers();
if (serversToPoll > MAX_SERVERS_TO_POLL)
{
serversToPoll = MAX_SERVERS_TO_POLL;
}
// Make the API call
for (int i = 0; i < serversToPoll; i++)
{
Object callResult = caller.call(API_METHOD_REQUEST_PUBKEY, hexPayload, addressVersion);
String resultString = callResult.toString();
Log.d(TAG, "The result of the 'request pubkey from server' API call was: " + resultString);
if ((resultString.equals(RESULT_CODE_REQUEST_PUBKEY)) == false) // If the call was successful
{
try
{
// Parse the JSON
JSONObject jObject = new JSONObject(resultString);
JSONArray jArray = jObject.getJSONArray(JSON_NAME_PUBKEY_PAYLOAD);
JSONObject object = jArray.getJSONObject(0); // There should never be more than one result for a 'request pubkey' call
String pubkeyHex = object.getString(JSON_NAME_DATA);
long payloadByteSize = pubkeyHex.length() / 2;
if (payloadByteSize < MAX_PAYLOAD_SIZE_TO_ACCEPT)
{
// Decode the pubkey data from hex
byte[] pubkeyData = ByteFormatter.hexStringToByteArray(pubkeyHex);
// Validate the pubkey
PubkeyProcessor pubProc = new PubkeyProcessor();
Pubkey pubkey = pubProc.reconstructPubkey(pubkeyData, addressString);
// Validate the reconstructed pubkey.
boolean pubkeyValid = pubProc.validatePubkey(pubkey, addressString);
if (pubkeyValid == false)
{
Log.i(TAG, "While running ServerCommunicator.requestPubkeyFromServer() in order to retrieve the pubkey \n" +
"for address " + addressString + ", a pubkey was reconstructed successfully from \n" +
"a payload provided by a server, but the resulting pubkey was found to be invalid.");
caller.switchToNextServer();
}
else
{
return pubkey;
}
}
else
{
long payloadKilobytes = payloadByteSize / 1000;
Log.d(TAG, "While running ServerCommunicator.requestPubkeyFromServer(), we received a payload that was larger than "
+ "the maximum size we are willing to accept. It has been ignored. \n"
+ "The size of the rejected payload was " + payloadKilobytes + " kilobytes.");
}
}
catch (JSONException e)
{
throw new RuntimeException("JSONException occcurred in ServerCommunicator.requestPubkeyFromServer(). \n" +
"The exception message was " + e.getLocalizedMessage() + "\n" +
"The API call result string was: " + resultString);
}
}
else
{
try
{
caller.switchToNextServer();
}
catch (RuntimeException e)
{
break;
}
}
}
// If we tried all the servers and none of them returned the correct pubkey
throw new RuntimeException("Failed to retrieve the requested pubkey after trying all servers.");
}
/**
* Requests any new msgs for each of our addresses, using the stream number of each address
* and the 'lastMsgCheckTime' value to shape the request.
*
* @param address - The Bitmessage Address which we want to check for messages sent to.
*/
public void checkServerForNewMsgs()
{
// Work out the time values to use in this request
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(App.getContext());
long lastMsgCheckTime = prefs.getLong(LAST_MSG_CHECK_TIME, 0);
long receivedSinceTime = calculateReceivedSinceTime(lastMsgCheckTime);
long receivedBeforeTime = calculateReceivedBeforeTime(receivedSinceTime, MAXIMUM_MSG_CATCH_UP_PERIOD);
// Get the stream numbers of all our addresses
PubkeyProvider pubProv = PubkeyProvider.get(App.getContext());
ArrayList<Pubkey> myPubkeys = pubProv.searchPubkeys(PubkeysTable.COLUMN_BELONGS_TO_ME, String.valueOf(1));
ArrayList<Integer> myStreamNumbers = new ArrayList<Integer>();
for (Pubkey p : myPubkeys)
{
Integer streamNumber = p.getStreamNumber();
if (myStreamNumbers.contains(streamNumber) == false)
{
myStreamNumbers.add(streamNumber);
}
}
// For each stream number of ours, check for any new msgs in that stream
for (Integer streamNumber : myStreamNumbers)
{
Log.d(TAG, "Making a server request for any msgs in stream " + streamNumber + " received between " + receivedSinceTime + " and " + receivedBeforeTime +
" - a period of " + TimeUtils.getTimeMessage((receivedBeforeTime - receivedSinceTime)) + ". " + TimeUtils.getLastMsgCheckTimeMessage() + ".");
// Work out how many servers to poll in this request
ApiCaller caller = new ApiCaller();
int serversToPoll = caller.getNumberOfServers();
if (serversToPoll > MAX_SERVERS_TO_POLL)
{
serversToPoll = MAX_SERVERS_TO_POLL;
}
// Make the API call
for (int i = 0; i < serversToPoll; i++)
{
Object callResult = caller.call(API_METHOD_CHECK_FOR_NEW_MSGS, streamNumber, receivedSinceTime, receivedBeforeTime);
String resultString = callResult.toString();
if ((resultString.equals(RESULT_CODE_CHECK_FOR_NEW_MSGS)) == false) // If the call was successful
{
try
{
ArrayList<String> msgStrings = new ArrayList<String>();
// Parse the JSON
JSONObject jObject = new JSONObject(resultString);
JSONArray jArray = jObject.getJSONArray(JSON_NAME_MSG_PAYLOADS);
for (int index = 0; index < jArray.length(); index++)
{
JSONObject object = jArray.getJSONObject(index);
String msgHex = object.getString(JSON_NAME_DATA);
long payloadByteSize = msgHex.length() / 2;
if (payloadByteSize < MAX_PAYLOAD_SIZE_TO_ACCEPT)
{
msgStrings.add(msgHex);
}
else
{
long payloadKilobytes = payloadByteSize / 1000;
Log.d(TAG, "While running ServerCommunicator.checkServerForNewMsgs(), we received a payload that was larger than "
+ "the maximum size we are willing to accept. It has been ignored. \n"
+ "The size of the rejected payload was " + payloadKilobytes + " kilobytes.");
}
}
// For each retrieved msg payload, check whether we have already received it
int newPayloads = 0;
for (String msgPayloadString : msgStrings)
{
byte[] msgBytes = ByteFormatter.hexStringToByteArray(msgPayloadString);
String msgBase64 = Base64.encodeToString(msgBytes, Base64.DEFAULT);
PayloadProvider payProv = PayloadProvider.get(App.getContext());
ArrayList<Payload> retrievedPayloads = payProv.searchPayloads(PayloadsTable.COLUMN_PAYLOAD, msgBase64);
if (retrievedPayloads.size() == 0)
{
Payload msgPayload = new Payload();
msgPayload.setBelongsToMe(false);
msgPayload.setProcessingComplete(false);
msgPayload.setType(Payload.OBJECT_TYPE_MSG);
msgPayload.setPayload(msgBytes);
payProv.addPayload(msgPayload);
newPayloads ++;
}
}
Log.d(TAG, "Out of the " + msgStrings.size() + " msg payloads returned by the server, " + newPayloads + " were new.");
if ((i + 1) < serversToPoll) // Do not attempt to switch to a new server if we have finished making all our API calls
{
caller.switchToNextServer();
}
}
catch (JSONException e)
{
throw new RuntimeException("JSONException occcurred in ServerCommunicator.checkServerForNewMsgs(). \n" +
"The exception message was " + e.getLocalizedMessage() + "\n" +
"The API call result string was: " + resultString);
}
}
else
{
Log.d(TAG, "The result of the 'check for new msgs' API call was: " + resultString);
try
{
caller.switchToNextServer();
}
catch (RuntimeException e)
{
break;
}
}
}
}
// After trying all the selected servers, if no exceptions were thrown, update the 'last successful msg check time'
SharedPreferences.Editor editor = prefs.edit();
editor.putLong(LAST_MSG_CHECK_TIME, receivedBeforeTime);
editor.commit();
Log.i(TAG, "Updated the 'last successful msg check time' value stored in SharedPreferences to " + receivedBeforeTime);
}
/**
* Calculates the 'received since' time value that should be used when checking for
* new objects.
*
* @param lastCheckTime - A long representing the time at which the last successful
* check was completed for the address in question
*
* @return A long containing the calculated 'received since' time value
*/
private long calculateReceivedSinceTime(long lastCheckTime)
{
if (lastCheckTime == 0) // If this is the first time we have checked for msgs sent to this address
{
return (System.currentTimeMillis() / 1000) - FIRST_CHECK_RECEIVED_TIME_MODIFIER;
}
else
{
return lastCheckTime - DEFAULT_RECEIVED_TIME_MODIFIER;
}
}
/**
* Calculates the 'received before' time value that should be used when checking for
* new objects.
*
* @param lastCheckTime - A long representing 'received since' time value that has
* been calculated for this request
* @param catchUpPeriod - A long containing the 'catch up period' to use. This should
* be a time value in seconds
*
* @return A long containing the calculated 'received before' time value
*/
private long calculateReceivedBeforeTime(long receivedSinceTime, long catchUpPeriod)
{
// If the 'received since' time is a long time in the past, modify it so that we aren't overwhelmed
// by a huge influx of new objects to process. Instead we will gradually catch up over time.
long currentTime = System.currentTimeMillis() / 1000;
long receivedBeforeTime = currentTime; // By default we use the current time
long timeGap = currentTime - receivedSinceTime;
if (timeGap > catchUpPeriod)
{
receivedBeforeTime = receivedSinceTime + catchUpPeriod;
}
return receivedBeforeTime;
}
}