package org.bitseal.network; import java.net.MalformedURLException; import java.net.URL; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collections; import java.util.Random; import org.bitseal.core.App; import org.bitseal.data.ServerRecord; import org.bitseal.database.ServerRecordProvider; import android.util.Log; import de.timroes.axmlrpc.XMLRPCClient; import de.timroes.axmlrpc.XMLRPCException; /** * An object which uses the XMLRPC client class to connect to servers running * PyBitmessage and call methods from the PyBitmessage API. * * @author Jonathan Coe */ public class ApiCaller { private URL url; private String username; private String password; private XMLRPCClient newClient; private XMLRPCClient client; private ArrayList<URL> urlList; private ArrayList<String> usernameList; private ArrayList<String> passwordList; private int urlCounter; private int usernameCounter; private int passwordCounter; private int numberOfServers; /** * This constant defines the timeout period for API calls. */ private static final int TIMEOUT_SECONDS = 10; /** * API command used for connection testing */ private static final String API_METHOD_ADD = "add"; private static final String TAG = "API_CALLER"; /** * Creates a new ApiCaller object and sets the URL, username, and password values needed * to connect to the PyBitmessage servers. */ public ApiCaller() { // Check if any server records exist in app storage. If not, set up the default list of server records. ServerRecordProvider servProv = ServerRecordProvider.get(App.getContext()); ArrayList<ServerRecord> retrievedServerRecords = servProv.getAllServerRecords(); if (retrievedServerRecords.size() == 0) { Log.i(TAG, "No server records found in app storage. Setting up list of default servers."); ServerHelper servHelp = new ServerHelper(); servHelp.setupDefaultServers(); // Now the server records should be available from the database retrievedServerRecords = servProv.getAllServerRecords(); } numberOfServers = retrievedServerRecords.size(); // Set up ArrayLists for the URLs, usernames, and passwords of the servers urlList = new ArrayList<URL>(); usernameList = new ArrayList<String>(); passwordList = new ArrayList<String>(); // Randomize the order of the server records list in order to avoid servers always being called in // the same order. Collections.shuffle(retrievedServerRecords, new SecureRandom()); int arrayListIndex = 0; for(ServerRecord s : retrievedServerRecords) { try { urlList.add(arrayListIndex, new URL(s.getURL())); usernameList.add(arrayListIndex, s.getUsername()); passwordList.add(arrayListIndex, s.getPassword()); arrayListIndex ++; } catch (MalformedURLException e) { Log.e(TAG, "Malformed URL exception occurred in ApiCaller constructor. We will ignore the ServerRecord that contains this " + "url. The String representation of the url was " + s.getURL()); arrayListIndex ++; } } // Start at the beginning of each of the three lists urlCounter = 0; usernameCounter = 0; passwordCounter = 0; url = urlList.get(urlCounter); username = usernameList.get(usernameCounter); password = passwordList.get(passwordCounter); client = setUpClient(url, username, password); Log.i(TAG, "ApiCaller setup completed"); } /** * Makes a call to the PyBitmessage XMLRPC API. <br><br> * * Attempts to establish a connection to one of the listed servers. The method will attempt to * connect to each server in sequence, until either a connection is successfully established or * all servers have been tested without any successful connection. If a connection is successfully * established, then the API call will be made. * * @param method - A String which specifies the API method to be called * @param params - One or more Objects which provide the parameters for the API call * * @return An Object containing the result of the API call */ public Object call(String method, Object... params) { while (urlCounter < urlList.size()) { boolean connectionSuccessful = doConnectionTest(); if (connectionSuccessful) { Log.i(TAG, "Successfully connected to " + url.toString()); try { Log.i(TAG, "About to make an API call to " + url.toString()); Object result = client.call(method, params); return result; } catch (XMLRPCException e) { Log.e(TAG, "XMLRPCException occurred in ApiCaller.call() \n" + "Execption message was: " + e.getMessage()); switchToNextServer(); } catch (IllegalStateException e) { Log.e(TAG, "IllegalStateException occurred in ApiCaller.call() \n" + "Execption message was: " + e.getMessage()); switchToNextServer(); } catch (Exception e) { Log.e(TAG, "An Exception occurred in ApiCaller.call() \n" + "Execption message was: " + e.getMessage()); switchToNextServer(); } } else { switchToNextServer(); } } throw new RuntimeException("API call failed after trying all listed servers. Last attempted URL was " + url.toString()); } /** * Sets up the XMLRPC client to use the next server in the list. If the end * of the list has been reached, throws a RuntimeException. */ public void switchToNextServer() { if (urlCounter < (urlList.size() - 1)) { Log.i(TAG, "Currently the URL in use is " + url.toString() + ", about to change to next URL"); urlCounter ++; usernameCounter ++; passwordCounter ++; url = urlList.get(urlCounter); username = usernameList.get(usernameCounter); password = passwordList.get(passwordCounter); client = setUpClient(url, username, password); } else { throw new RuntimeException("API call failed after trying all listed servers. Last attempted URL was " + url.toString()); } } /** * Returns the number of servers in use. */ public int getNumberOfServers() { return numberOfServers; } /** * Performs a connection test by calling the "add" method from the PyBitmessage API and * checking if the returned result (if any) is correct. * * @return A boolean indicating whether or not a connection was successfully established */ private boolean doConnectionTest() { Object rawResult = null; try { Log.i(TAG, "Running doConnectionTest() with server at " + url.toString()); int result = -1; // Explicitly set this value to ensure a meaningful test. The testInt values will always be >=0, so the test should never give a false positive result. Random rand = new Random(); int testInt1 = rand.nextInt(5000); int testInt2 = rand.nextInt(5000); int sumOfTestInts = testInt1 + testInt2; rawResult = client.call(API_METHOD_ADD, testInt1, testInt2); // Test the connection with some random values that are unlikely to come up by chance result = (Integer) rawResult; if (result == sumOfTestInts) { return true; } else { return false; } } catch (XMLRPCException e) { Log.e(TAG, "XMLRPCException occurred in ApiCaller.doConnectionTest() \n" + "Execption message was: " + e.getMessage()); e.printStackTrace(); return false; } catch (IllegalStateException e) { Log.e(TAG, "IllegalStateException occurred in ApiCaller.doConnectionTest() \n" + "Execption message was: " + e.getMessage()); return false; } catch (Exception e) { Log.e(TAG, "An Exception occurred in ApiCaller.doConnectionTest() \n" + "Execption message was: " + e.getMessage()); Log.e(TAG, "The raw result of the connection test was: " + rawResult.toString()); return false; } } /** * Sets up a XMLRPC client and provides it with the data necessary to connect with * the server via the XMLRPC API. * * @param url A URL object containing the IP address and port number of the server, in the form "http://23.21.148.16:8442" * @param username A String containing the username required to access the PyBitmessage API. Specified in the server's * local copy of the "keys.dat" file * @param password A String containing the password required to access the PyBitmessage API. Specified in the server's * local copy of the "keys.dat" file * * @return An XMLRPCClient object that can be used to make XMLRPC calls to Bitseal servers */ private XMLRPCClient setUpClient(URL url, String username, String password) { newClient = new XMLRPCClient(url); newClient.setLoginData(username, password); newClient.setTimeout(TIMEOUT_SECONDS); return newClient; } }