Android: DNS Java SRV lookup fails when network connectivity changes - java

Happy Friday, everyone!
My Android app relies on DNS Java (http://www.dnsjava.org/doc/) to perform an SRV lookup. We just discovered that if network connectivity changes (Switch from LTE to WiFi or vice versa) each lookup from then on will perpetually timeout regardless of whether internet connectivity is established again on the new network. Sometimes this is resolved by switching back to the old network, but if you change too many times it will not recover. Here is the code we're using below.
If I can provide any other details please let me know. Help is much appreciated!
private static class ConfigUpdater extends AsyncTask<Void, Void, JsonConfig> {
private static final String SRV_RUE_CONFIG_PREFIX = "_rueconfig._tls.";
ConfigListener listener;
String request_url;
String query_url;
String username, password;
String errorMsg;
public ConfigUpdater(String url, String username, String password, ConfigListener listener) {
this.username = username;
this.password = password;
this.listener = listener;
query_url = SRV_RUE_CONFIG_PREFIX + url;
errorMsg = "Failed to Login";
org.xbill.DNS.ResolverConfig.refresh();
}
#Override
protected JsonConfig doInBackground(Void... params) {
Record[] records;// = new Record[0];
try {
Lookup configLookup = new Lookup(query_url, Type.SRV);
configLookup.setCache(null);
records = configLookup.run();
} catch (TextParseException e) {
e.printStackTrace();
return null;
}
if(records != null && records.length > 0) {
for (Record record : records) {
SRVRecord srv = (SRVRecord) record;
String hostname = srv.getTarget().toString().replaceFirst("\\.$", "");
request_url = "https://" + hostname + "/config/v1/config.json";
Log.d("Auto Config request_url: "+request_url);
}
try {
String reponse_str = getFromHttpURLConnection();
Log.d("Auto Config JSON: "+reponse_str);
return parseJson(username, reponse_str, request_url);
} catch (Throwable e){
Log.d("Issue parsing json");
e.printStackTrace();
}
}
return null;
}

For anyone who may ever struggle with SRV lookups failing after a network switch I seem to have found the problem. The issue was that the Resolver property on the Lookup object seems to cache network configuration. If you want to ensure your lookups always have the latest network configuration, simply reinitialize a new Resolver. With the modifications below I was unable to reproduce any of the initial test cases.
Record[] records = null;// = new Record[0];
try {
Lookup configLookup = new Lookup(query_url, Type.SRV);
configLookup.setResolver(new ExtendedResolver()); /** FIX **/
records = configLookup.run();
} catch (TextParseException e) {
e.printStackTrace();
return null;
} catch (UnknownHostException e) {
e.printStackTrace();
}

Related

Using SharedPreferences data in Application class

I am developing an app and I use Socket.io on it, I initialize the socket in a class that extends Application and looks like this:
public class Inicio extends Application{
private Socket mSocket;
private SharedPreferences spref;
#Override
public void onCreate(){
super.onCreate();
try{
spref = getSharedPreferences("accountData", Context.MODE_PRIVATE);
IO.Options op = new IO.Options();
op.forceNew = true;
op.reconnection = true;
op.query = "tok=" + spref.getString("sessiontoken", "") + "&usr=" + spref.getString("userid", "");
mSocket = IO.socket(Constants.serverAddress, op);
}catch(URISyntaxException e){
throw new RuntimeException(e);
}
}
public Socket getmSocket(){
return mSocket;
}
}
So I can get and use the same socket instance in other parts of my application's code calling the following way:
Inicio appClass = (Inicio) getApplication();
mSocket = appClas.getmSocket();
mSocket.connect();
But there is a small problem that motivated me to post this question, can you see when I call to SharedPreferences in the Application class? I do this because I need to send the session token and user account ID to properly start the socket connection with my server, the problem is:
Imagine that a user opens the app for the first time and does not have an account yet, he will login or register and then the session token and user ID will be saved to SharedPreferences, but when the app started and ran the Application class, SharedPreferences was still empty and did not have the required token and user ID to establish the connection, so the user would have to reopen the app now to be able to use the socket successfully.
So I ask you: What are my alternative options for solving the problem? Is there another structure besides the Application class that I could use to not suffer from this problem? Or is there some way to bypass this problem?
What I'm doing to get around the problem for now is to restart the app programmatically when login occurs but I believe this is looks like a sad joke and not the ideal way to do it.
Thanks, I apologize for this long question of mine, but I'll be grateful for any help.
Separate your soket creation logic like below:
private void createSoket() {
spref = getSharedPreferences("accountData", Context.MODE_PRIVATE);
String sessiontoken = spref.getString("sessiontoken", "");
String userId = spref.getString("userid", "");
if(!(TextUtils.isEmpty(sessiontoken) || TextUtils.isEmpty(userId))) {
try {
IO.Options op = new IO.Options();
op.forceNew = true;
op.reconnection = true;
op.query = "tok=" + sessiontoken + "&usr=" + userId;
mSocket = IO.socket(Constants.serverAddress, op);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
}
And when required soket check null and create before pass the instance.
public Socket getmSocket(){
if(mSoket == null)
createSoket();
return mSocket;
}
N.B: Without valid settionToken and userId, soket is null
This is complete Application class:
public class Inicio extends Application{
private Socket mSocket;
private SharedPreferences spref;
#Override
public void onCreate(){
super.onCreate();
createSoket();
}
private void createSoket() {
spref = getSharedPreferences("accountData", Context.MODE_PRIVATE);
String sessiontoken = spref.getString("sessiontoken", "");
String userId = spref.getString("userid", "");
if(!(TextUtils.isEmpty(sessiontoken) || TextUtils.isEmpty(userId))) {
try {
IO.Options op = new IO.Options();
op.forceNew = true;
op.reconnection = true;
op.query = "tok=" + sessiontoken + "&usr=" + userId;
mSocket = IO.socket(Constants.serverAddress, op);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
}
public Socket getmSocket(){
if(mSoket == null)
createSoket();
return mSocket;
}
}

BufferingResponseListener and getContentAsString append the previously fetched content

I run a custom WebSocketServlet for Jetty, which sends short text push notifications (for an async mobile and desktop word game) to many platforms (Facebook, Vk.com, Mail.ru, Ok.ru also Firebase and Amazon messaging) using a Jetty HttpClient instance:
public class MyServlet extends WebSocketServlet {
private final SslContextFactory mSslFactory = new SslContextFactory();
private final HttpClient mHttpClient = new HttpClient(mSslFactory);
#Override
public void init() throws ServletException {
super.init();
try {
mHttpClient.start();
} catch (Exception ex) {
throw new ServletException(ex);
}
mFcm = new Fcm(mHttpClient); // Firebase
mAdm = new Adm(mHttpClient); // Amazon
mApns = new Apns(mHttpClient); // Apple
mFacebook = new Facebook(mHttpClient);
mMailru = new Mailru(mHttpClient);
mOk = new Ok(mHttpClient);
mVk = new Vk(mHttpClient);
}
This has worked very good for the past year, but since I have recently upgraded my WAR-file to use Jetty 9.4.14.v20181114 the trouble has begun -
public class Facebook {
private final static String APP_ID = "XXXXX";
private final static String APP_SECRET = "XXXXX";
private final static String MESSAGE_URL = "https://graph.facebook.com/%s/notifications?" +
// the app access token is: "app id | app secret"
"access_token=%s%%7C%s" +
"&template=%s";
private final HttpClient mHttpClient;
public Facebook(HttpClient httpClient) {
mHttpClient = httpClient;
}
private final BufferingResponseListener mMessageListener = new BufferingResponseListener() {
#Override
public void onComplete(Result result) {
if (!result.isSucceeded()) {
LOG.warn("facebook failure: {}", result.getFailure());
return;
}
try {
// THE jsonStr SUDDENLY CONTAINS PREVIOUS CONTENT!
String jsonStr = getContentAsString(StandardCharsets.UTF_8);
LOG.info("facebook success: {}", jsonStr);
} catch (Exception ex) {
LOG.warn("facebook exception: ", ex);
}
}
};
public void postMessage(int uid, String sid, String body) {
String url = String.format(MESSAGE_URL, sid, APP_ID, APP_SECRET, UrlEncoded.encodeString(body));
mHttpClient.POST(url).send(mMessageListener);
}
}
Suddenly the getContentAsString method called for successful HttpClient invocations started to deliver the strings, which were fetched previously - prepended to the the actual result string.
What could it be please, is it some changed BufferingResponseListener behaviour or maybe some non-obvious Java quirk?
BufferingResponseListener was never intended to be reusable across requests.
Just allocate a new BufferingResponseListener for every request/response.

AWS EC2 Instances: Launch Multiple Instance

I want to create ec2 instances when ever the new user arrive. I created a servlet class to do this. When User arrive i check DB that is user new or not if new then create the instance and send back his/her IP. When i send http request to this servlet one by one for users i get the IP correctly. But when i send HTTP Call in parallel (for user1 send request in tab1, for user2 send request in tab2 simultaneously before getting response from user1 HTTP call). When i do this i got error. Sometimes user1 said
"The instance ID 'i-0b79495934c3b5459' does not exist (Service:
AmazonEC2; Status Code: 400; Error Code: InvalidInstanceID.NotFound;
Request ID: e18a9eaa-cb1b-4130-a3ee-bf1b19fa184c) "
And user2 send IP in response. Kindly help me What is the issue and how to resolve this.
This is the Servlet Class which i created.
public class GateKeeperController extends HttpServlet {
private static final long serialVersionUID = 1L;
BasicAWSCredentials awsCreds = new BasicAWSCredentials(credentials);
AmazonEC2Client ec2Client = new AmazonEC2Client(awsCreds);
RunInstancesRequest runInstancesRequest;
RunInstancesResult runInstancesResult;
Reservation reservation;
Instance intstance;
DescribeInstancesRequest describeInstanceRequest;
DescribeInstancesResult describeInstanceResult;
GatekeeperModal gateKeepermodal;
String sourceAMI = null;
String destinationAMI = null;
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpSession s = request.getSession();
String userID = (String) request.getParameter("userID");
Double lattitude = Double.parseDouble((String) request.getParameter("lat"));
Double lonitude = Double.parseDouble((String) request.getParameter("long"));
if (userID != null) {
Pair coordinates = new Pair(lattitude, lonitude);
RegionSelection targetRegion = new RegionSelection();
String regionResult = targetRegion.getRegion(coordinates);
String instanceIP = null;
gateKeepermodal = new GatekeeperModal();
try {
if (gateKeepermodal.checkUserIsNew(userID)) {
instanceIP = startInstance(userID, regionResult);
if (instanceIP != null) {
response.getWriter().write(instanceIP);
}
}
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} else {
}
}
private String startInstance(String userID, String region) {
String ami_id = new AMI().getAMI_ID(region);
ec2Client.setEndpoint(region);
runInstancesRequest = new RunInstancesRequest();
runInstancesRequest.withImageId(ami_id).withInstanceType("t2.micro").withMinCount(1).withMaxCount(1)
.withKeyName("GateKeeper_User").withSecurityGroups("GateKeeper User");
runInstancesResult = ec2Client.runInstances(runInstancesRequest);
reservation = runInstancesResult.getReservation();
intstance = reservation.getInstances().get(0);
String s1 = intstance.getState().getName();
String s2 = InstanceStateName.Running.name();
while (!s1.toLowerCase().equals(s2.toLowerCase())) {
describeInstanceRequest = new DescribeInstancesRequest();
describeInstanceRequest.withInstanceIds(intstance.getInstanceId());
ec2Client.setEndpoint(region);
describeInstanceResult = ec2Client.describeInstances(describeInstanceRequest);
reservation = describeInstanceResult.getReservations().get(0);
intstance = reservation.getInstances().get(0);
s1 = intstance.getState().getName();
s2 = InstanceStateName.Running.name();
}
GateKeeperUser user = new GateKeeperUser(userID, intstance.getInstanceId(), intstance.getPublicIpAddress(),
region);
Boolean result;
try {
result = gateKeepermodal.createUser(user);
if (result) {
return intstance.getPublicIpAddress();
} else {
return null;
}
} catch (SQLException e) {
}
return null;
}
}
According to the documentation:
"If you successfully run the RunInstances command, and then
immediately run another command using the instance ID that was
provided in the response of RunInstances, it may return an
InvalidInstanceID.NotFound error. This does not mean the instance does
not exist. Some specific commands that may be affected are:
DescribeInstances: To confirm the actual state of the instance, run
this command using an exponential backoff algorithm.
TerminateInstances: To confirm the state of the instance, first run
the DescribeInstances command using an exponential backoff algorithm."

CloseableHttpClient execute takes so much time

When I make a call to the method CloseableHttpClient.execute it takes so much time to finish the first time I call it. For example, if I call an API call 10 times in a for bucle, the first call takes much more time than the rest of the call and I don't know the reason.
I would appreciate if someone can help.
Regards.
public static void main(String[] args) throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, Exception
{
AcanoAPICallsTest2 test = new AcanoAPICallsTest2();
AcanoAPIHandler clientHandler = test.getClientHandler();
if (clientHandler.getConnectCode() == HttpStatus.SC_OK) {
int statusCode = clientHandler.executeMethod(CommonSettings.GET, "/api/xxx);
}
}
clientHandler.shutDownClient();
}
public class AcanoAPIHandler extends ClientHandler
{
protected Logger logger = Logger.getLogger(this.getClass());
private final String LOCATION = "Location";
private String location;
// private int connectCode = HttpStatus.SC_SERVICE_UNAVAILABLE;
/**
* Returns the "Location" field of the response header (if exists)
*
* #return
*/
public String getLocationHeaderResponse()
{
return location;
}
// default constructor
public AcanoAPIHandler()
{
super();
}
public AcanoAPIHandler(String protocol, String host, Integer port, String username, String password)
{
super(protocol, host, port, username, password);
}
#Override
public int executeMethod(String type, String path, List<BasicNameValuePair>... nvps)
{
int statusCode = super.executeMethod(type, path, nvps);
this.location = null;
if (type.equalsIgnoreCase(CommonSettings.POST) || type.equalsIgnoreCase(CommonSettings.PUT))
{
// if statusCode is 200, set the location header
if (statusCode == HttpStatus.SC_OK)
{
Header[] h = this.getResponse().getAllHeaders();
for (int i = 0; i < h.length; i++)
{
if (h[i].getName().equalsIgnoreCase(LOCATION))
{
String locationStr = h[i].getValue();
String[] split = locationStr.split("/");
if (split.length > 0)
{
this.location = split[split.length - 1];
break;
}
}
}
}
}
return statusCode;
}
}
ClientHandler.executeMethod
public int executeMethod(String type, String path, List<BasicNameValuePair>... nvps)
{
int statusCode = -1;
HttpUriRequest request = createUriRequest(type, path);
this.responseContent = null;
this.response = null;
try
{
if (nvps.length > 0)
{
if (type.equalsIgnoreCase(CommonSettings.POST))
{
((HttpPost) request).setEntity(new UrlEncodedFormEntity(nvps[0], "UTF-8"));
}
else if (type.equalsIgnoreCase(CommonSettings.PUT))
{
((HttpPut) request).setEntity(new UrlEncodedFormEntity(nvps[0], "UTF-8"));
}
else
{
logger.warn("Can only set entity on POST/PUT operation, ignoring nvps");
}
}
}
catch (UnsupportedEncodingException ex)
{
java.util.logging.Logger.getLogger(ClientHandler.class.getName()).log(Level.SEVERE, null, ex);
}
if (this.httpclient != null)
{
try
{
long start = System.currentTimeMillis();
this.response = this.httpclient.execute(request);
long end = System.currentTimeMillis();
long res = end - start;
System.out.println("httpclient.execute " + " seconds: "+res/1000);
statusCode = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity();
if (entity != null)
{
InputStream fis = entity.getContent();
this.responseContent = convertStreamToString(fis);
EntityUtils.consume(entity);
fis.close();
}
}
catch (IOException ex)
{
java.util.logging.Logger.getLogger(ClientHandler.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
return SERVER_ERROR;
}
finally
{
// release connection
if (type.equalsIgnoreCase(CommonSettings.GET))
{
((HttpGet) request).releaseConnection();
}
else if (type.equalsIgnoreCase(CommonSettings.POST))
{
((HttpPost) request).releaseConnection();
}
else if (type.equalsIgnoreCase(CommonSettings.PUT))
{
((HttpPut) request).releaseConnection();
}
else if (type.equalsIgnoreCase(CommonSettings.DELETE))
{
((HttpDelete) request).releaseConnection();
}
// close the response
try
{
if (this.response != null)
{
this.response.close();
}
}
catch (IOException ex)
{
java.util.logging.Logger.getLogger(ClientHandler.class.getName()).log(Level.SEVERE, null, ex);
return SERVER_ERROR;
}
}
}
return statusCode;
}
I don't see how this.httpclient is initialized in ClientHandler class, but usually this happens when you are executing request to a host which is far away from you and uses reusable http connections (this is why the first request is noticeably slower than others).
When you open TCP connection to a host, TCP three way handshake is made. This means that you have to wait before connection is established and only after that actual HTTP request is sent. Establishing connection from Europe to somewhere in North America would take ~90ms and more. Ping time from London to other cities
Using TCP connection multiple times is a good practice. Because after TCP connection is established and the first request is done you can send new requests without extra waiting time. You can read more about HTTP persistent connection
Seems that you are connecting to Acano servers, I don't know exactly where their Data Center(-s) is/are, cause they can have couple of them across the world, but the company is located in USA. So seems legit in case you are not very close to the Arcano's Data Center.

Constructing the most reliable user country mechanism

In an application that I'm currently working on there is a huge need to determine user country as fast as possible and as reliable as possible. I have decided to rely on three ways for finding user country; each one has its advantages and disadvantages:
Android inner methods to get the SIM country.
GeoCoding.
IP to Location API.
Here are the three pieces of code:
1. Android inner methods to get the SIM country:
public static String getUserCountry(Context context) {
try {
final TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
final String simCountry = tm.getSimCountryIso();
if (simCountry != null && simCountry.length() == 2) { // SIM country code is available
CupsLog.d(TAG, "getUserCountry, simCountry: " + simCountry.toLowerCase(Locale.US));
return simCountry.toLowerCase(Locale.US);
}
else if (tm.getPhoneType() != TelephonyManager.PHONE_TYPE_CDMA) { // device is not 3G (would be unreliable)
String networkCountry = tm.getNetworkCountryIso();
if (networkCountry != null && networkCountry.length() == 2) { // network country code is available
CupsLog.d(TAG, "getUserCountry, networkCountry: " + networkCountry.toLowerCase(Locale.US));
return networkCountry.toLowerCase(Locale.US);
}
}
}
catch (Exception e) { }
return null;
}
2. GeoCoding:
public static void getCountryCode(final Location location, final Context context) {
CupsLog.d(TAG, "getCountryCode");
AsyncTask<Void, Void, String> countryCodeTask = new AsyncTask<Void, Void, String>() {
final float latitude = (float) location.getLatitude();
final float longitude = (float) location.getLongitude();
List<Address> addresses = null;
Geocoder gcd = new Geocoder(context);
String code = null;
#Override
protected String doInBackground(Void... params) {
CupsLog.d(TAG, "doInBackground");
try {
addresses = gcd.getFromLocation(latitude, longitude, 10);
for (int i=0; i < addresses.size(); i++)
{
if (addresses.get(i) != null && addresses.get(i).getCountryCode() != null)
{
code = addresses.get(i).getCountryCode();
}
}
} catch (IOException e) {
CupsLog.d(TAG, "IOException" + e);
}
return code;
}
#Override
protected void onPostExecute(String code)
{
if (code != null)
{
CupsLog.d(TAG, "getCountryCode :" + code.toLowerCase());
UserLocation.instance.setCountryCode(code.toLowerCase());
CookieUtil.formatLangueageAndLocationCookie();
CookieUtil.instance.instantateCookieUtil(context);
}
}
};
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) {
CupsLog.d(TAG, "countryCodeTask.execute();");
countryCodeTask.execute();
} else {
CupsLog.d(TAG, "countryCodeTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);");
countryCodeTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
3. IP to Location API:
private void getUserCountryByIp() {
AsyncHttpClient client = new AsyncHttpClient();
client.setCookieStore(CookieUtil.instance.getPersistentCookieStore());
String userCountryApi = "http://ip-api.com/json";
CupsLog.i(TAG, "country uri: " + userCountryApi);
client.get(userCountryApi, new JsonHttpResponseHandler() {
#Override
public void onSuccess(JSONObject orderResponseJSON) {
CupsLog.i(TAG, "onSuccess(JSONObject res)");
try
{
CupsLog.i(TAG, "JsonResponse: "+ orderResponseJSON.toString(3));
String tempString = orderResponseJSON.getString("countryCode");
if (tempString != null)
{
//countryCodeFromIpApi = tempString.toLowerCase();
UserLocation.instance.setCountryCode(tempString.toLowerCase());
CookieUtil.formatLangueageAndLocationCookie();
CookieUtil.instance.instantateCookieUtil(LoginActivity.this);
isGotCountryFromIp = true;
}
} catch (JSONException e) {
CupsLog.i(TAG, "JSONException: " + e);
}
}
#Override
public void onFailure(Throwable arg0, JSONObject orderResponseJSON) {
CupsLog.i(TAG, "onFailure");
try {
CupsLog.i(TAG, "JsonResponse: "+ orderResponseJSON.toString(3));
} catch (JSONException e) {
CupsLog.i(TAG, "JSONException: " + e);
}
}
#Override
public void onFinish() {
CupsLog.i(TAG, "onFinish");
super.onFinish();
}
});
}
Now I have those 3 methods that are working great, my problem is more of a Java problem. The first method give me the result right away, while the two others (2,3) are potentiality long running tasks. what more is that the first option is the most not reliable one, as users can travel to different countries with the SIM card. The second is more reliable but still sometimes does not returns an country (depending on the location of the user). The third one is the one that I found to be the most reliable one, but the most long as well.
The question: knowing this information, how would I construct a method that uses those 3 methods in the right order for reliability stand point and considering the long running tasks factor?
Unfortunately there is no really reliable way to determine the physical location of a user (e.g. his/her cellphone) because:
SIM card might be bought and/or manufactured in other country;
Geocoding (which is AFAIU based on GPS/GLONASS coordinates) might give wrong (~10m) results or no results at all if user disabled it or no satellites are visible (underground, for example);
Resolving country by IP might also give you wrong results, for example because of using VPN;
So my advice would be to ask user, which country he is currently in and willing to "tell" you so.

Categories

Resources