com.irccloud.android.HTTPFetcher.java Source code

Java tutorial

Introduction

Here is the source code for com.irccloud.android.HTTPFetcher.java

Source

/*
 * Copyright (c) 2016 IRCCloud, Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.irccloud.android;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;

import com.codebutler.android_websockets.HybiParser;
import com.crashlytics.android.Crashlytics;
import com.datatheorem.android.trustkit.TrustKit;

import org.apache.http.Header;
import org.apache.http.HttpException;
import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpResponseException;
import org.apache.http.conn.ssl.StrictHostnameVerifier;
import org.apache.http.message.BasicLineParser;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.zip.GZIPInputStream;

import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;

@TargetApi(8)
public class HTTPFetcher {
    private static final int MAX_THREADS = 6;
    private static final String TAG = "HTTPFetcher";

    protected URL mURI;
    protected Socket mSocket;
    protected Thread mThread;
    protected String mProxyHost;
    protected int mProxyPort;
    protected boolean isCancelled;

    private static final String ENABLED_CIPHERS[] = { "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
            "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
            "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", "TLS_DHE_RSA_WITH_AES_128_CBC_SHA",
            "TLS_DHE_RSA_WITH_AES_256_CBC_SHA", "TLS_ECDHE_RSA_WITH_RC4_128_SHA",
            "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA",
            "SSL_RSA_WITH_3DES_EDE_CBC_SHA", "SSL_RSA_WITH_RC4_128_SHA", "SSL_RSA_WITH_RC4_128_MD5", };

    private static final String ENABLED_PROTOCOLS[] = { "TLSv1.2", "TLSv1.1", "TLSv1" };

    public HTTPFetcher(URL uri) {
        mURI = uri;

        mProxyHost = System.getProperty("http.proxyHost", null);
        try {
            mProxyPort = Integer.parseInt(System.getProperty("http.proxyPort", "8080"));
        } catch (NumberFormatException e) {
            mProxyPort = -1;
        }

        if (mProxyHost != null && mProxyHost.length() > 0
                && (mProxyHost.equalsIgnoreCase("localhost") || mProxyHost.equalsIgnoreCase("127.0.0.1")))
            mProxyHost = null;
    }

    public void cancel() {
        Crashlytics.log(Log.INFO, TAG, "HTTP request cancelled");
        isCancelled = true;
    }

    private static final ArrayList<Thread> mSocketThreads = new ArrayList<>();
    private final ArrayList<Thread> mCurrentSocketThreads = new ArrayList<>();
    private int mAddressCount;
    private int mAttempts;

    private class ConnectRunnable implements Runnable {
        private SocketFactory mSocketFactory;
        private InetSocketAddress mAddress;

        ConnectRunnable(SocketFactory factory, InetSocketAddress address) {
            mSocketFactory = factory;
            mAddress = address;
        }

        @Override
        public void run() {
            try {
                Crashlytics.log(Log.INFO, TAG, "Connecting to address: " + mAddress.getAddress() + " port: "
                        + mAddress.getPort() + " (attempt " + mAttempts + ")");
                Socket socket = mSocketFactory.createSocket();
                socket.connect(mAddress, 30000);
                if (mSocket == null) {
                    mSocket = socket;
                    Crashlytics.log(Log.INFO, TAG,
                            "Connected to " + mAddress.getAddress() + " (attempt " + mAttempts + ")");
                    if (mURI.getProtocol().equals("https")) {
                        SSLSocket s = (SSLSocket) mSocket;
                        try {
                            s.setEnabledProtocols(ENABLED_PROTOCOLS);
                        } catch (IllegalArgumentException e) {
                            //Not supported on older Android versions
                        }
                        try {
                            s.setEnabledCipherSuites(ENABLED_CIPHERS);
                        } catch (IllegalArgumentException e) {
                            //Not supported on older Android versions
                        }
                    }
                    mThread = Thread.currentThread();
                    http_thread();
                } else {
                    socket.close();
                }
            } catch (Exception ex) {
                ex.printStackTrace();
                if (mSocket == null) {
                    NetworkConnection.printStackTraceToCrashlytics(ex);
                }
            }
            mSocketThreads.remove(Thread.currentThread());
            mCurrentSocketThreads.remove(Thread.currentThread());
            if (mSocket == null && mCurrentSocketThreads.size() == 0 && mAttempts == mAddressCount) {
                Crashlytics.log(Log.ERROR, TAG, "Failed to connect after " + mAttempts + " attempts");
                onFetchFailed();
            }
        }
    }

    public void connect() {
        if (mThread != null && mThread.isAlive()) {
            return;
        }

        mThread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    if (isCancelled)
                        return;

                    Crashlytics.log(Log.INFO, TAG, "Requesting: " + mURI);
                    int port = (mURI.getPort() != -1) ? mURI.getPort()
                            : (mURI.getProtocol().equals("https") ? 443 : 80);
                    SocketFactory factory = mURI.getProtocol().equals("https") ? getSSLSocketFactory()
                            : SocketFactory.getDefault();
                    if (mProxyHost != null && mProxyHost.length() > 0 && mProxyPort > 0) {
                        Crashlytics.log(Log.INFO, TAG,
                                "Connecting to proxy: " + mProxyHost + " port: " + mProxyPort);
                        mSocket = SocketFactory.getDefault().createSocket(mProxyHost, mProxyPort);
                        mThread = new Thread(new Runnable() {
                            @SuppressLint("NewApi")
                            public void run() {
                                http_thread();
                            }
                        });
                        mThread.setName("http-stream-thread");
                        mThread.start();
                    } else {
                        InetAddress[] addresses = InetAddress.getAllByName(mURI.getHost());
                        mAddressCount = addresses.length;
                        for (InetAddress address : addresses) {
                            if (mSocket == null && !isCancelled) {
                                if (mSocketThreads.size() >= MAX_THREADS) {
                                    Crashlytics.log(Log.INFO, TAG,
                                            "Waiting for other HTTP requests to complete before continuing");

                                    while (mSocketThreads.size() >= MAX_THREADS) {
                                        Thread.sleep(1000);
                                    }
                                }
                                Thread t = new Thread(
                                        new ConnectRunnable(factory, new InetSocketAddress(address, port)));
                                mSocketThreads.add(t);
                                mCurrentSocketThreads.add(t);
                                mAttempts++;
                                t.start();
                                Thread.sleep(300);
                            } else {
                                break;
                            }
                        }
                    }
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        });
        mThread.start();
    }

    private void http_thread() {
        try {
            mThread.setName("http-stream-thread");
            int port = (mURI.getPort() != -1) ? mURI.getPort() : (mURI.getProtocol().equals("https") ? 443 : 80);

            String path = TextUtils.isEmpty(mURI.getPath()) ? "/" : mURI.getPath();
            if (!TextUtils.isEmpty(mURI.getQuery())) {
                path += "?" + mURI.getQuery();
            }

            PrintWriter out = new PrintWriter(mSocket.getOutputStream());

            if (mProxyHost != null && mProxyHost.length() > 0 && mProxyPort > 0) {
                out.print("CONNECT " + mURI.getHost() + ":" + port + " HTTP/1.0\r\n");
                out.print("\r\n");
                out.flush();
                HybiParser.HappyDataInputStream stream = new HybiParser.HappyDataInputStream(
                        mSocket.getInputStream());

                // Read HTTP response status line.
                StatusLine statusLine = parseStatusLine(readLine(stream));
                if (statusLine == null) {
                    throw new HttpException("Received no reply from server.");
                } else if (statusLine.getStatusCode() != HttpStatus.SC_OK) {
                    throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase());
                }

                // Read HTTP response headers.
                while (!TextUtils.isEmpty(readLine(stream)))
                    ;
                if (mURI.getProtocol().equals("https")) {
                    mSocket = getSSLSocketFactory().createSocket(mSocket, mURI.getHost(), port, false);
                    SSLSocket s = (SSLSocket) mSocket;
                    try {
                        s.setEnabledProtocols(ENABLED_PROTOCOLS);
                    } catch (IllegalArgumentException e) {
                        //Not supported on older Android versions
                    }
                    try {
                        s.setEnabledCipherSuites(ENABLED_CIPHERS);
                    } catch (IllegalArgumentException e) {
                        //Not supported on older Android versions
                    }
                    out = new PrintWriter(mSocket.getOutputStream());
                }
            }

            if (mURI.getProtocol().equals("https")) {
                SSLSocket s = (SSLSocket) mSocket;
                StrictHostnameVerifier verifier = new StrictHostnameVerifier();
                if (!verifier.verify(mURI.getHost(), s.getSession()))
                    throw new SSLException("Hostname mismatch");
            }

            Crashlytics.log(Log.DEBUG, TAG, "Sending HTTP request");

            out.print("GET " + path + " HTTP/1.0\r\n");
            out.print("Host: " + mURI.getHost() + "\r\n");
            if (mURI.getHost().equals(NetworkConnection.IRCCLOUD_HOST)
                    && NetworkConnection.getInstance().session != null
                    && NetworkConnection.getInstance().session.length() > 0)
                out.print("Cookie: session=" + NetworkConnection.getInstance().session + "\r\n");
            out.print("Connection: close\r\n");
            out.print("Accept-Encoding: gzip\r\n");
            out.print("User-Agent: " + NetworkConnection.getInstance().useragent + "\r\n");
            out.print("\r\n");
            out.flush();

            HybiParser.HappyDataInputStream stream = new HybiParser.HappyDataInputStream(mSocket.getInputStream());

            // Read HTTP response status line.
            StatusLine statusLine = parseStatusLine(readLine(stream));
            if (statusLine != null)
                Crashlytics.log(Log.DEBUG, TAG, "Got HTTP response: " + statusLine);

            if (statusLine == null) {
                throw new HttpException("Received no reply from server.");
            } else if (statusLine.getStatusCode() != HttpStatus.SC_OK
                    && statusLine.getStatusCode() != HttpStatus.SC_MOVED_PERMANENTLY) {
                Crashlytics.log(Log.ERROR, TAG, "Failure: " + mURI + ": " + statusLine.getStatusCode() + " "
                        + statusLine.getReasonPhrase());
                throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase());
            }

            // Read HTTP response headers.
            String line;

            boolean gzipped = false;
            while (!TextUtils.isEmpty(line = readLine(stream))) {
                Header header = parseHeader(line);
                if (header.getName().equalsIgnoreCase("content-encoding")
                        && header.getValue().equalsIgnoreCase("gzip"))
                    gzipped = true;
                if (statusLine.getStatusCode() == HttpStatus.SC_MOVED_PERMANENTLY
                        && header.getName().equalsIgnoreCase("location")) {
                    Crashlytics.log(Log.INFO, TAG, "Redirecting to: " + header.getValue());
                    mURI = new URL(header.getValue());
                    mSocket.close();
                    mSocket = null;
                    mThread = null;
                    connect();
                    return;
                }
            }

            if (gzipped)
                onStreamConnected(new GZIPInputStream(mSocket.getInputStream()));
            else
                onStreamConnected(mSocket.getInputStream());

            onFetchComplete();
        } catch (Exception ex) {
            NetworkConnection.printStackTraceToCrashlytics(ex);
            onFetchFailed();
        }
    }

    protected void onFetchComplete() {

    }

    protected void onFetchFailed() {

    }

    protected void onStreamConnected(InputStream stream) throws Exception {

    }

    private StatusLine parseStatusLine(String line) {
        if (TextUtils.isEmpty(line)) {
            return null;
        }
        return BasicLineParser.parseStatusLine(line, new BasicLineParser());
    }

    private Header parseHeader(String line) {
        return BasicLineParser.parseHeader(line, new BasicLineParser());
    }

    // Can't use BufferedReader because it buffers past the HTTP data.
    private String readLine(HybiParser.HappyDataInputStream reader) throws IOException {
        int readChar = reader.read();
        if (readChar == -1) {
            return null;
        }
        StringBuilder string = new StringBuilder("");
        while (readChar != '\n') {
            if (readChar != '\r') {
                string.append((char) readChar);
            }

            readChar = reader.read();
            if (readChar == -1) {
                return null;
            }
        }
        return string.toString();
    }

    private SSLSocketFactory getSSLSocketFactory() throws NoSuchAlgorithmException, KeyManagementException {
        SSLContext context = SSLContext.getInstance("TLS");

        TrustManager[] trustManagers = null;

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            trustManagers = new TrustManager[1];
            trustManagers[0] = TrustKit.getInstance().getTrustManager(mURI.getHost());
        }

        context.init(null, trustManagers, null);
        return context.getSocketFactory();
    }
}