Commit cba50d81 authored by Bartek Fabiszewski's avatar Bartek Fabiszewski
Browse files

Fix: on API < 19 connection fails if SSL is disabled on server

parent 9f82e52d
Loading
Loading
Loading
Loading
+108 −0
Original line number Diff line number Diff line
/*
 * Copyright (c) 2017 Bartek Fabiszewski
 * http://www.fabiszewski.net
 *
 * This file is part of μlogger-android.
 * Licensed under GPL, either version 3, or any later.
 * See <http://www.gnu.org/licenses/>
 */

package net.fabiszewski.ulogger;

import android.content.Context;
import android.net.SSLCertificateSocketFactory;
import android.net.SSLSessionCache;
import android.util.Log;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.Socket;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;

/**
 * This custom ssl socket factory will be used only with API < 19
 * to solve problem with connecting to SSL-disabled servers.
 */

@SuppressWarnings("deprecation")
class TlsSocketFactory extends SSLSocketFactory {

    private static final String TAG = TlsSocketFactory.class.getSimpleName();
    private static HostnameVerifier hostnameVerifier;
    private static SSLSocketFactory factory;

    TlsSocketFactory(Context context) {
        SSLSessionCache cache = new SSLSessionCache(context);
        factory = SSLCertificateSocketFactory.getDefault(WebHelper.SOCKET_TIMEOUT, cache);
        hostnameVerifier = new org.apache.http.conn.ssl.BrowserCompatHostnameVerifier();
    }

    @Override
    public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException {
        if (autoClose) { socket.close(); }

        socket = factory.createSocket(InetAddress.getByName(host), port);

        if (socket != null && socket instanceof SSLSocket) {
            if (Logger.DEBUG) { Log.d(TAG, "[Preparing TLS socket]"); }
            SSLSocket sslSocket = (SSLSocket) socket;

            // set all protocols including TLS (disabled by default on older APIs)
            sslSocket.setEnabledProtocols(sslSocket.getSupportedProtocols());
            sslSocket.setEnabledCipherSuites(sslSocket.getSupportedCipherSuites());

            if (host != null && !host.isEmpty()) {
                // set hostname for SNI
                if (Logger.DEBUG) { Log.d(TAG, "[Setting SNI for host " + host + "]"); }
                try {
                    Method setHostnameMethod = sslSocket.getClass().getMethod("setHostname", String.class);
                    setHostnameMethod.invoke(sslSocket, host);
                } catch (Exception e) {
                    if (Logger.DEBUG) { Log.d(TAG, "[Setting SNI failed: " + e.getMessage() + "]"); }
                }
            }
            // verify hostname and certificate
            SSLSession session = sslSocket.getSession();
            if (!hostnameVerifier.verify(host, session)) {
                throw new SSLPeerUnverifiedException("Hostname '" + host + "' was not verified (" + session.getPeerPrincipal() + ")");
            }

            if (Logger.DEBUG) { Log.d(TAG, "Connected to " + session.getPeerHost() + " using " + session.getProtocol() + " (" + session.getCipherSuite() + ")"); }
        }
        return socket;
    }

    @Override
    public String[] getDefaultCipherSuites() {
        throw new UnsupportedOperationException("Not implemented");
    }

    @Override
    public String[] getSupportedCipherSuites() {
        throw new UnsupportedOperationException("Not implemented");
    }

    @Override
    public Socket createSocket(String host, int port) throws IOException {
        throw new UnsupportedOperationException("Not implemented");
    }

    @Override
    public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
        throw new UnsupportedOperationException("Not implemented");
    }

    @Override
    public Socket createSocket(InetAddress host, int port) throws IOException {
        throw new UnsupportedOperationException("Not implemented");
    }

    @Override
    public Socket createSocket(InetAddress host, int port, InetAddress localHost, int localPort) throws IOException {
        throw new UnsupportedOperationException("Not implemented");
    }
}
+56 −23
Original line number Diff line number Diff line
@@ -11,6 +11,7 @@ package net.fabiszewski.ulogger;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.preference.PreferenceManager;
import android.util.Log;

@@ -32,6 +33,9 @@ import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;

/**
 * Web server communication
 *
@@ -77,6 +81,10 @@ class WebHelper {
    private final String userAgent;
    private final Context context;

    private static boolean tlsSocketInitialized = false;
    // Socket timeout in milliseconds
    static final int SOCKET_TIMEOUT = 30 * 1000;


    /**
     * Constructor
@@ -87,13 +95,27 @@ class WebHelper {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        user = prefs.getString("prefUsername", "NULL");
        pass = prefs.getString("prefPass", "NULL");
        host = prefs.getString("prefHost", "NULL");
        userAgent = context.getString(R.string.app_name_ascii) + "; " + System.getProperty("http.agent");
        host = prefs.getString("prefHost", "NULL").replaceAll("/+$", "");
        userAgent = context.getString(R.string.app_name_ascii) + "/" + BuildConfig.VERSION_NAME + "; " + System.getProperty("http.agent");

        if (cookieManager == null) {
            cookieManager = new CookieManager();
            CookieHandler.setDefault(cookieManager);
        }

        // On API < 19 connection fails if SSL is disabled on server
        // Try with TLS enabled socket
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT && !tlsSocketInitialized) {
            try {
                if (Logger.DEBUG) { Log.d(TAG, "[init TLS socket factory]"); }
                SSLSocketFactory tlsFactory = new TlsSocketFactory(context);
                HttpsURLConnection.setDefaultSSLSocketFactory(tlsFactory);
                tlsSocketInitialized = true;
            } catch (Exception e) {
                if (Logger.DEBUG) { Log.d(TAG, "[TLS socket setup error (ignored): " + e.getMessage() + "]"); }
            }

        }
    }

    /**
@@ -121,6 +143,8 @@ class WebHelper {
        byte[] data = dataString.getBytes();

        HttpURLConnection connection = null;
        InputStream in = null;
        OutputStream out = null;
        try {
            boolean redirect;
            int redirectTries = 5;
@@ -131,10 +155,13 @@ class WebHelper {
                connection.setRequestMethod("POST");
                connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
                connection.setRequestProperty("Content-Length", Integer.toString(data.length));
                connection.setRequestProperty("User-agent", userAgent);
                connection.setInstanceFollowRedirects(true);
                connection.setRequestProperty("User-Agent", userAgent);
                connection.setInstanceFollowRedirects(false);
                connection.setConnectTimeout(SOCKET_TIMEOUT);
                connection.setReadTimeout(SOCKET_TIMEOUT);
                connection.setUseCaches(true);

                OutputStream out = new BufferedOutputStream(connection.getOutputStream());
                out = new BufferedOutputStream(connection.getOutputStream());
                out.write(data);
                out.flush();

@@ -157,6 +184,13 @@ class WebHelper {
                    if (h1 != null && !h1.equalsIgnoreCase(h2)) {
                        throw new IOException(context.getString(R.string.e_illegal_redirect, responseCode));
                    }
                    try {
                        out.close();
                        connection.getInputStream().close();
                        connection.disconnect();
                    } catch (final IOException e) {
                        if (Logger.DEBUG) { Log.d(TAG, "[connection cleanup failed (ignored)]"); }
                    }
                }
                else if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
                    throw new WebAuthException(context.getString(R.string.e_auth_failure, responseCode));
@@ -166,7 +200,7 @@ class WebHelper {
                }
            } while (redirect);

            InputStream in = new BufferedInputStream(connection.getInputStream());
            in = new BufferedInputStream(connection.getInputStream());

            StringBuilder sb = new StringBuilder();
            BufferedReader br = new BufferedReader(new InputStreamReader(in));
@@ -175,21 +209,20 @@ class WebHelper {
                sb.append(inputLine);
            }
            response = sb.toString();
        } catch (IOException e) {
            int responseCode;
            if (connection != null && (responseCode = connection.getResponseCode()) > 0) {
                if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
                    // eg. IOException: Received authentication challenge is null
                    throw new WebAuthException(context.getString(R.string.e_auth_failure, responseCode));
                } else {
                    throw new IOException(context.getString(R.string.e_http_code, responseCode));
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
                if (in != null) {
                    in.close();
                }
            throw e;
        } finally {
                if (connection != null) {
                    connection.disconnect();
                }
            } catch (final IOException e) {
                if (Logger.DEBUG) { Log.d(TAG, "[connection cleanup failed (ignored)]"); }
            }
        }
        if (Logger.DEBUG) { Log.d(TAG, "[postWithParams response: " + response + "]"); }
        return response;