4

I'm writing an Android app that connects to a cPanel server (Apache 2.2.22) page which is password protected. When the authentication credentials are correct, I have no problem connecting. However, when the credentials are incorrect, my Android application seems to freeze in the HttpURLConnection.getResponseCode() method. The logs on the server show hundreds of requests being sent from my Android device, all returning a 401 as expected, but for some reason this is not reflected in my application.

Here is my code, executed from within an AsyncTask:

    @Override
    protected Integer doInBackground(String... bookInfoString) {
        // Stop if cancelled
        if(isCancelled()){
            return null;
        }
        Log.i(getClass().getName(), "SendToDatabase.doInBackground()");

        String apiUrlString = getResources().getString(R.string.url_vages_library);
        try{
            NetworkConnection connection = new NetworkConnection(apiUrlString);
            connection.appendPostData(bookInfoString[0]);
            int responseCode = connection.getResponseCode();
            Log.d(getClass().getName(), "responseCode: " + responseCode);
            return responseCode;
        } catch(IOException e) {
            return null;
        }

    }

This code makes use of my own class NetworkConnection, which is just a basic wrapper class around an HttpURLConnection, to avoid repeating code. Here it is:

public class NetworkConnection {

    private String url;
    private HttpURLConnection connection;

    public NetworkConnection(String urlString) throws IOException{
        Log.i(getClass().getName(), "Building NetworkConnection for the URL \"" + urlString + "\"");

        url = urlString;
        // Build Connection.
        try{
            URL url = new URL(urlString);
            connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("GET");
            connection.setReadTimeout(1000 /* 1 seconds */);
            connection.setConnectTimeout(1000 /* 1 seconds */);
        } catch (MalformedURLException e) {
            // Impossible: The only two URLs used in the app are taken from string resources.
            e.printStackTrace();
        } catch (ProtocolException e) {
            // Impossible: "GET" is a perfectly valid request method.
            e.printStackTrace();
        }
    }

    public void appendPostData(String postData) {

        try{
            Log.d(getClass().getName(), "appendPostData() called.\n" + postData);

            Log.d(getClass().getName(), "connection.getConnectTimeout(): " + connection.getConnectTimeout());
            Log.d(getClass().getName(), "connection.getReadTimeout(): " + connection.getReadTimeout());

            // Modify connection settings.
            connection.setRequestMethod("POST");
            connection.setDoOutput(true);
            connection.setRequestProperty("Content-Type", "application/json");

            // Get OutputStream and attach POST data.
            OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream(), "UTF-8");
            writer.write(postData);
            if(writer != null){
                writer.flush();
                writer.close();
            }

        } catch (SocketTimeoutException e) {
            Log.w(getClass().getName(), "Connection timed out.");
        } catch (ProtocolException e) {
            // Impossible: "POST" is a perfectly valid request method.
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            // Impossible: "UTF-8" is a perfectly valid encoding.
            e.printStackTrace();
        } catch (IOException e) {
            // Pretty sure this is impossible but not 100%.
            e.printStackTrace();
        }
    }

    public int getResponseCode() throws IOException{
        Log.i(getClass().getName(), "getResponseCode()");
        int responseCode = connection.getResponseCode();
        Log.i(getClass().getName(), "responseCode: " + responseCode);
        return responseCode;
    }

    public void disconnect(){
        Log.i(getClass().getName(), "disconnect()");
        connection.disconnect();
    }
}

And finally, here is a fraction of the logcat logs:

05-03 11:01:16.315: D/vages.library.NetworkConnection(3408): connection.getConnectTimeout(): 1000
05-03 11:01:16.315: D/vages.library.NetworkConnection(3408): connection.getReadTimeout(): 1000
05-03 11:01:16.585: I/vages.library.NetworkConnection(3408): getResponseCode()
05-03 11:04:06.395: I/vages.library.MainActivity$SendToDatabase(3408): SendToDatabase.onPostExecute(null)

You can see the the method seems to just return null after a random amount of time. The longest I have waited was exactly 15 minutes. There are also several memory logs (GC_CONCURRENT) from dalikvm between the last two info logs which I have omitted.

I should also say that at the moment I am not using https, although I do not believe that should cause any problems. I would be very grateful for any feedback with this, whether it's a complete answer or just a comment telling me what isn't the problem, as I am still unsure whether this problem is server-side or client-side.

Thank you very much, William

EDIT: I forgot to mention before, I am attaching my authentication credentials with my own custom java.net.Authenticator:

public class CustomAuthenticator extends Authenticator {

    Context mContext;

    public CustomAuthenticator(Context context){
        super();
        mContext = context;
    }

    @Override
    protected PasswordAuthentication getPasswordAuthentication() {

        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
        String username = sharedPreferences.getString(SettingsActivity.KEY_USERNAME_PREFERENCE, null);
        String password = sharedPreferences.getString(SettingsActivity.KEY_PASSWORD_PREFERENCE, null);

        return new PasswordAuthentication(username, password.toCharArray());
    }
}

which I set in the activity'sonCreate() method:

Authenticator.setDefault(new CustomAuthenticator(mContext));

Also, I have used curl to request the password protected resource, and have received a 401 as expected. I am now assuming the problem is client-side.

William Carter
  • 1,287
  • 12
  • 19
  • 1
    I'm sorry maybe I'm blind but I cannot see right now where do you send your auth credentials. Anyway, maybe you can try the request using curl to see what kind of output do you get: `curl -v "http://yoururl.com" -u user:password`. If it doesn't work the problem will probably be in the server side. – Esparver May 03 '13 at 11:36
  • Thanks @Esparver, I've tried it in curl and it's returning a 401 as expected. So I guess it's a client-side problem then. I am sending my auth credentials using a java.net.Authenticator. I'll attach this code to my answer. – William Carter May 03 '13 at 11:54

3 Answers3

3

It seems to be an issue with using Authenticator in POST connections. It's quite old so I don't know if it still exists.

I would try two things:

  • Add a log line in the getPasswordAuthentication of the Authenticator to see if it's effectively called. If nothing is printed, you should check that you add the default Authenticator before it's called. You say you do it in the onCreate(), so it should be fine but it's good to be sure.
  • Avoid using the Authenticator (at least for testing purposes) and send the auth info directly in the HTTP Request. I usually do it this way:

    String auth = user + ":" + pass;
    conn = (HttpURLConnection) url.openConnection();
    conn.setRequestProperty("Authorization", 
                   "Basic " + Base64.encode(auth.getBytes()));
    // Set other parameters and read the result...
    
Esparver
  • 1,515
  • 1
  • 15
  • 28
  • Thank you for such a detailed answer. I put a log in `getPasswordAuthentication` and it's being called multiple times. It made me realise that a 401 is given when the request has no Authorization header **and** when the details are incorrect, so my app is automatically sending more requests with faulty auth credentials. According to REST, it is the client who should handle this, so I have put a retry counter in my `CustomAuthenticator`. It now works and I will post my answer, although I am still looking for alternatives. – William Carter May 03 '13 at 15:40
  • Using `HttpURLConneciton.setRequestProperty()` was actually how I was authenticating before. It works perfectly on devices running 3.0+, but for some reason on 2.3.4 and below, I am receiving 400s whether the credentials are correct or not, and the server logs don't record the request at all. I am still searching for an answer to this problem as well so would appreciate anything else you can suggest, although I may create a new question for it. I now have a workaround for this question, but I am still seeking knowledge and a real solution. – William Carter May 03 '13 at 15:44
  • I ran into the same problem when using the `Authenticator` with `PUT` requests. I'm going to change my `Authenticator` to give the user a chance to reenter the credentials or cancel the action altogether. – Daniel Jul 25 '13 at 13:34
2

The problem was that the 401 Unauthorized status is sent when the Authorization header is missing and when the credentials contained within the header are incorrect. Therefore, my app was constantly sending the same request over and over to no avail. I have therefore found a workaround to the problem by adding a counter into my CustomAuthenticator:

public class CustomAuthenticator extends Authenticator {

    public static int RETRIES = 3;

    int mRetriesLeft;
    Context mContext;

    public CustomAuthenticator(Context context){
        super();
        mRetriesLeft = RETRIES;
        mContext = context;
    }

    @Override
    protected PasswordAuthentication getPasswordAuthentication() {

        Log.i(getClass().getName(), "getPasswordAuthentication() - mCounter: " + mRetriesLeft);

        if(mRetriesLeft > 0){       

            SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
            String username = sharedPreferences.getString(SettingsActivity.KEY_USERNAME_PREFERENCE, null);
            String password = sharedPreferences.getString(SettingsActivity.KEY_PASSWORD_PREFERENCE, null);

            mRetriesLeft--;
            return new PasswordAuthentication(username, password.toCharArray());

        } else {
            Log.w(getClass().getName(), "No more retries. Returning null");
            mRetriesLeft = RETRIES;
            return null;
        }
    }

    public void reset(){
        mRetriesLeft = RETRIES;
    }
}

I should say however that I do not like this solution and therefore, have not accepted it. You have to remember to reset the counter whenever you make a new request (I do it in AsyncTask.onPreExecute()), or else every third request will fail. Also, I'm sure there must be a native way to do this, although after scouring the documentation I can't find it. I would still be very grateful if anyone can point it out to me.

William Carter
  • 1,287
  • 12
  • 19
0

I don't know if I am right or not but my solution has worked for me for a whole day without a glitch.

Try doing this

byte[] buf = new byte[4096];
Inputstream is;
do
{
    http conn code etc;
    is=conn.getInputStream();

    if(is.read(buf)==0)             
    {
        flag=1;
    }

    //u can either is.close(); or leave as is

    //code

    int serverResponseCode = connection.getResponseCode();
    String serverResponseMessage = connection.getResponseMessage();     
    conn.disconnect();

} while(flag==1);
Erik
  • 2,137
  • 3
  • 25
  • 42