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}