HttpConnectionManager.java

  1package eu.siacs.conversations.http;
  2
  3import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
  4
  5import android.content.Context;
  6import android.os.Build;
  7import android.util.Log;
  8
  9import eu.siacs.conversations.BuildConfig;
 10import eu.siacs.conversations.Config;
 11import eu.siacs.conversations.crypto.TrustManagers;
 12import eu.siacs.conversations.entities.Account;
 13import eu.siacs.conversations.entities.Message;
 14import eu.siacs.conversations.services.AbstractConnectionManager;
 15import eu.siacs.conversations.services.XmppConnectionService;
 16import eu.siacs.conversations.utils.TLSSocketFactory;
 17
 18import okhttp3.HttpUrl;
 19import okhttp3.OkHttpClient;
 20import okhttp3.Request;
 21import okhttp3.ResponseBody;
 22
 23import org.apache.http.conn.ssl.StrictHostnameVerifier;
 24
 25import java.io.IOException;
 26import java.io.InputStream;
 27import java.net.InetAddress;
 28import java.net.InetSocketAddress;
 29import java.net.Proxy;
 30import java.net.UnknownHostException;
 31import java.security.KeyManagementException;
 32import java.security.KeyStoreException;
 33import java.security.NoSuchAlgorithmException;
 34import java.security.cert.CertificateException;
 35import java.util.ArrayList;
 36import java.util.List;
 37import java.util.concurrent.Executor;
 38import java.util.concurrent.Executors;
 39import java.util.concurrent.TimeUnit;
 40
 41import javax.net.ssl.SSLSocketFactory;
 42import javax.net.ssl.X509TrustManager;
 43
 44public class HttpConnectionManager extends AbstractConnectionManager {
 45
 46    private final List<HttpDownloadConnection> downloadConnections = new ArrayList<>();
 47    private final List<HttpUploadConnection> uploadConnections = new ArrayList<>();
 48
 49    public static final Executor EXECUTOR = Executors.newFixedThreadPool(4);
 50
 51    private static final OkHttpClient OK_HTTP_CLIENT;
 52
 53    static {
 54        OK_HTTP_CLIENT = new OkHttpClient.Builder()
 55                .addInterceptor(chain -> {
 56                    final Request original = chain.request();
 57                    final Request modified = original.newBuilder()
 58                            .header("User-Agent", getUserAgent())
 59                            .build();
 60                    return chain.proceed(modified);
 61                })
 62                .build();
 63    }
 64
 65
 66    public static String getUserAgent() {
 67        return String.format("%s/%s", BuildConfig.APP_NAME, BuildConfig.VERSION_NAME);
 68    }
 69
 70    public HttpConnectionManager(XmppConnectionService service) {
 71        super(service);
 72    }
 73
 74    public static Proxy getProxy() {
 75        final InetAddress localhost;
 76        try {
 77            localhost = InetAddress.getByAddress(new byte[]{127, 0, 0, 1});
 78        } catch (final UnknownHostException e) {
 79            throw new IllegalStateException(e);
 80        }
 81        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
 82            return new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(localhost, 9050));
 83        } else {
 84            return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(localhost, 8118));
 85        }
 86    }
 87
 88    public void createNewDownloadConnection(Message message) {
 89        this.createNewDownloadConnection(message, false);
 90    }
 91
 92    public void createNewDownloadConnection(final Message message, boolean interactive) {
 93        synchronized (this.downloadConnections) {
 94            for (HttpDownloadConnection connection : this.downloadConnections) {
 95                if (connection.getMessage() == message) {
 96                    Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": download already in progress");
 97                    return;
 98                }
 99            }
100            final HttpDownloadConnection connection = new HttpDownloadConnection(message, this);
101            connection.init(interactive);
102            this.downloadConnections.add(connection);
103        }
104    }
105
106    public void createNewUploadConnection(final Message message, boolean delay) {
107        synchronized (this.uploadConnections) {
108            for (HttpUploadConnection connection : this.uploadConnections) {
109                if (connection.getMessage() == message) {
110                    Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": upload already in progress");
111                    return;
112                }
113            }
114            HttpUploadConnection connection = new HttpUploadConnection(message, Method.determine(message.getConversation().getAccount()), this);
115            connection.init(delay);
116            this.uploadConnections.add(connection);
117        }
118    }
119
120    void finishConnection(HttpDownloadConnection connection) {
121        synchronized (this.downloadConnections) {
122            this.downloadConnections.remove(connection);
123        }
124    }
125
126    void finishUploadConnection(HttpUploadConnection httpUploadConnection) {
127        synchronized (this.uploadConnections) {
128            this.uploadConnections.remove(httpUploadConnection);
129        }
130    }
131
132    OkHttpClient buildHttpClient(final HttpUrl url, final Account account, boolean interactive) {
133        return buildHttpClient(url, account, 30, interactive);
134    }
135
136    OkHttpClient buildHttpClient(final HttpUrl url, final Account account, int readTimeout, boolean interactive) {
137        final String slotHostname = url.host();
138        final boolean onionSlot = slotHostname.endsWith(".onion");
139        final OkHttpClient.Builder builder = OK_HTTP_CLIENT.newBuilder();
140        builder.writeTimeout(30, TimeUnit.SECONDS);
141        builder.readTimeout(readTimeout, TimeUnit.SECONDS);
142        setupTrustManager(builder, interactive);
143        if (mXmppConnectionService.useTorToConnect() || account.isOnion() || onionSlot) {
144            builder.proxy(HttpConnectionManager.getProxy()).build();
145        }
146        return builder.build();
147    }
148
149    private void setupTrustManager(final OkHttpClient.Builder builder, final boolean interactive) {
150        final X509TrustManager trustManager;
151        if (interactive) {
152            trustManager = mXmppConnectionService.getMemorizingTrustManager().getInteractive();
153        } else {
154            trustManager = mXmppConnectionService.getMemorizingTrustManager().getNonInteractive();
155        }
156        try {
157            final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, SECURE_RANDOM);
158            builder.sslSocketFactory(sf, trustManager);
159            builder.hostnameVerifier(new StrictHostnameVerifier());
160        } catch (final KeyManagementException | NoSuchAlgorithmException ignored) {
161        }
162    }
163
164    public static InputStream open(final String url, final boolean tor) throws IOException {
165        return open(HttpUrl.get(url), tor);
166    }
167
168    public static InputStream open(final HttpUrl httpUrl, final boolean tor) throws IOException {
169        final OkHttpClient.Builder builder = OK_HTTP_CLIENT.newBuilder();
170        if (tor) {
171            builder.proxy(HttpConnectionManager.getProxy()).build();
172        }
173        final OkHttpClient client = builder.build();
174        final Request request = new Request.Builder().get().url(httpUrl).build();
175        final ResponseBody body = client.newCall(request).execute().body();
176        if (body == null) {
177            throw new IOException("No response body found");
178        }
179        return body.byteStream();
180    }
181
182
183    public static OkHttpClient okHttpClient(final Context context) {
184        final OkHttpClient.Builder builder = HttpConnectionManager.OK_HTTP_CLIENT.newBuilder();
185        try {
186            final X509TrustManager trustManager;
187            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
188                trustManager = TrustManagers.defaultWithBundledLetsEncrypt(context);
189            } else {
190                trustManager = TrustManagers.createDefaultTrustManager();
191            }
192            final SSLSocketFactory socketFactory =
193                    new TLSSocketFactory(new X509TrustManager[] {trustManager}, SECURE_RANDOM);
194            builder.sslSocketFactory(socketFactory, trustManager);
195        } catch (final IOException
196                       | KeyManagementException
197                       | NoSuchAlgorithmException
198                       | KeyStoreException
199                       | CertificateException e) {
200            Log.d(Config.LOGTAG, "not reconfiguring service to work with bundled LetsEncrypt");
201            throw new RuntimeException(e);
202        }
203        return builder.build();
204    }
205}