1package eu.siacs.conversations.http;
2
3import android.content.Intent;
4import android.net.Uri;
5import android.os.SystemClock;
6
7import org.apache.http.conn.ssl.StrictHostnameVerifier;
8
9import java.io.BufferedInputStream;
10import java.io.IOException;
11import java.io.OutputStream;
12import java.net.HttpURLConnection;
13import java.net.MalformedURLException;
14import java.net.URL;
15import java.security.KeyManagementException;
16import java.security.NoSuchAlgorithmException;
17import java.util.Arrays;
18
19import javax.net.ssl.HostnameVerifier;
20import javax.net.ssl.HttpsURLConnection;
21import javax.net.ssl.SSLContext;
22import javax.net.ssl.SSLHandshakeException;
23import javax.net.ssl.SSLSocketFactory;
24import javax.net.ssl.X509TrustManager;
25
26import eu.siacs.conversations.Config;
27import eu.siacs.conversations.entities.Downloadable;
28import eu.siacs.conversations.entities.DownloadableFile;
29import eu.siacs.conversations.entities.Message;
30import eu.siacs.conversations.services.XmppConnectionService;
31import eu.siacs.conversations.utils.CryptoHelper;
32
33public class HttpConnection implements Downloadable {
34
35 private HttpConnectionManager mHttpConnectionManager;
36 private XmppConnectionService mXmppConnectionService;
37
38 private URL mUrl;
39 private Message message;
40 private DownloadableFile file;
41 private int mStatus = Downloadable.STATUS_UNKNOWN;
42 private boolean acceptedAutomatically = false;
43 private int mProgress = 0;
44 private long mLastGuiRefresh = 0;
45
46 public HttpConnection(HttpConnectionManager manager) {
47 this.mHttpConnectionManager = manager;
48 this.mXmppConnectionService = manager.getXmppConnectionService();
49 }
50
51 @Override
52 public boolean start() {
53 if (mXmppConnectionService.hasInternetConnection()) {
54 if (this.mStatus == STATUS_OFFER_CHECK_FILESIZE) {
55 checkFileSize(true);
56 } else {
57 new Thread(new FileDownloader(true)).start();
58 }
59 return true;
60 } else {
61 return false;
62 }
63 }
64
65 public void init(Message message) {
66 this.message = message;
67 this.message.setDownloadable(this);
68 try {
69 mUrl = new URL(message.getBody());
70 String[] parts = mUrl.getPath().toLowerCase().split("\\.");
71 String lastPart = parts.length >= 1 ? parts[parts.length - 1] : null;
72 String secondToLast = parts.length >= 2 ? parts[parts.length -2] : null;
73 if ("pgp".equals(lastPart) || "gpg".equals(lastPart)) {
74 this.message.setEncryption(Message.ENCRYPTION_PGP);
75 } else if (message.getEncryption() != Message.ENCRYPTION_OTR) {
76 this.message.setEncryption(Message.ENCRYPTION_NONE);
77 }
78 String extension;
79 if (Arrays.asList(VALID_CRYPTO_EXTENSIONS).contains(lastPart)) {
80 extension = secondToLast;
81 } else {
82 extension = lastPart;
83 }
84 message.setRelativeFilePath(message.getUuid()+"."+extension);
85 this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
86 String reference = mUrl.getRef();
87 if (reference != null && reference.length() == 96) {
88 this.file.setKey(CryptoHelper.hexToBytes(reference));
89 }
90
91 if (this.message.getEncryption() == Message.ENCRYPTION_OTR
92 && this.file.getKey() == null) {
93 this.message.setEncryption(Message.ENCRYPTION_NONE);
94 }
95 checkFileSize(false);
96 } catch (MalformedURLException e) {
97 this.cancel();
98 }
99 }
100
101 private void checkFileSize(boolean interactive) {
102 new Thread(new FileSizeChecker(interactive)).start();
103 }
104
105 public void cancel() {
106 mHttpConnectionManager.finishConnection(this);
107 message.setDownloadable(null);
108 mXmppConnectionService.updateConversationUi();
109 }
110
111 private void finish() {
112 Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
113 intent.setData(Uri.fromFile(file));
114 mXmppConnectionService.sendBroadcast(intent);
115 message.setDownloadable(null);
116 mHttpConnectionManager.finishConnection(this);
117 mXmppConnectionService.updateConversationUi();
118 if (acceptedAutomatically) {
119 mXmppConnectionService.getNotificationService().push(message);
120 }
121 }
122
123 private void changeStatus(int status) {
124 this.mStatus = status;
125 mXmppConnectionService.updateConversationUi();
126 }
127
128 private void setupTrustManager(final HttpsURLConnection connection,
129 final boolean interactive) {
130 final X509TrustManager trustManager;
131 final HostnameVerifier hostnameVerifier;
132 if (interactive) {
133 trustManager = mXmppConnectionService.getMemorizingTrustManager();
134 hostnameVerifier = mXmppConnectionService
135 .getMemorizingTrustManager().wrapHostnameVerifier(
136 new StrictHostnameVerifier());
137 } else {
138 trustManager = mXmppConnectionService.getMemorizingTrustManager()
139 .getNonInteractive();
140 hostnameVerifier = mXmppConnectionService
141 .getMemorizingTrustManager()
142 .wrapHostnameVerifierNonInteractive(
143 new StrictHostnameVerifier());
144 }
145 try {
146 final SSLContext sc = SSLContext.getInstance("TLS");
147 sc.init(null, new X509TrustManager[]{trustManager},
148 mXmppConnectionService.getRNG());
149
150 final SSLSocketFactory sf = sc.getSocketFactory();
151 final String[] cipherSuites = CryptoHelper.getOrderedCipherSuites(
152 sf.getSupportedCipherSuites());
153 if (cipherSuites.length > 0) {
154 sc.getDefaultSSLParameters().setCipherSuites(cipherSuites);
155
156 }
157
158 connection.setSSLSocketFactory(sf);
159 connection.setHostnameVerifier(hostnameVerifier);
160 } catch (final KeyManagementException | NoSuchAlgorithmException ignored) {
161 }
162 }
163
164 private class FileSizeChecker implements Runnable {
165
166 private boolean interactive = false;
167
168 public FileSizeChecker(boolean interactive) {
169 this.interactive = interactive;
170 }
171
172 @Override
173 public void run() {
174 long size;
175 try {
176 size = retrieveFileSize();
177 } catch (SSLHandshakeException e) {
178 changeStatus(STATUS_OFFER_CHECK_FILESIZE);
179 HttpConnection.this.acceptedAutomatically = false;
180 HttpConnection.this.mXmppConnectionService.getNotificationService().push(message);
181 return;
182 } catch (IOException e) {
183 cancel();
184 return;
185 }
186 file.setExpectedSize(size);
187 if (size <= mHttpConnectionManager.getAutoAcceptFileSize()) {
188 HttpConnection.this.acceptedAutomatically = true;
189 new Thread(new FileDownloader(interactive)).start();
190 } else {
191 changeStatus(STATUS_OFFER);
192 HttpConnection.this.acceptedAutomatically = false;
193 HttpConnection.this.mXmppConnectionService.getNotificationService().push(message);
194 }
195 }
196
197 private long retrieveFileSize() throws IOException,
198 SSLHandshakeException {
199 changeStatus(STATUS_CHECKING);
200 HttpURLConnection connection = (HttpURLConnection) mUrl
201 .openConnection();
202 connection.setRequestMethod("HEAD");
203 if (connection instanceof HttpsURLConnection) {
204 setupTrustManager((HttpsURLConnection) connection, interactive);
205 }
206 connection.connect();
207 String contentLength = connection.getHeaderField("Content-Length");
208 if (contentLength == null) {
209 throw new IOException();
210 }
211 try {
212 return Long.parseLong(contentLength, 10);
213 } catch (NumberFormatException e) {
214 throw new IOException();
215 }
216 }
217
218 }
219
220 private class FileDownloader implements Runnable {
221
222 private boolean interactive = false;
223
224 public FileDownloader(boolean interactive) {
225 this.interactive = interactive;
226 }
227
228 @Override
229 public void run() {
230 try {
231 changeStatus(STATUS_DOWNLOADING);
232 download();
233 updateImageBounds();
234 finish();
235 } catch (SSLHandshakeException e) {
236 changeStatus(STATUS_OFFER);
237 } catch (IOException e) {
238 cancel();
239 }
240 }
241
242 private void download() throws SSLHandshakeException, IOException {
243 HttpURLConnection connection = (HttpURLConnection) mUrl
244 .openConnection();
245 if (connection instanceof HttpsURLConnection) {
246 setupTrustManager((HttpsURLConnection) connection, interactive);
247 }
248 connection.connect();
249 BufferedInputStream is = new BufferedInputStream(
250 connection.getInputStream());
251 file.getParentFile().mkdirs();
252 file.createNewFile();
253 OutputStream os = file.createOutputStream();
254 if (os == null) {
255 throw new IOException();
256 }
257 long transmitted = 0;
258 long expected = file.getExpectedSize();
259 int count = -1;
260 byte[] buffer = new byte[1024];
261 while ((count = is.read(buffer)) != -1) {
262 transmitted += count;
263 os.write(buffer, 0, count);
264 updateProgress((int) ((((double) transmitted) / expected) * 100));
265 }
266 os.flush();
267 os.close();
268 is.close();
269 }
270
271 private void updateImageBounds() {
272 message.setType(Message.TYPE_IMAGE);
273 mXmppConnectionService.getFileBackend().updateFileParams(message,mUrl);
274 mXmppConnectionService.updateMessage(message);
275 }
276
277 }
278
279 public void updateProgress(int i) {
280 this.mProgress = i;
281 if (SystemClock.elapsedRealtime() - this.mLastGuiRefresh > Config.PROGRESS_UI_UPDATE_INTERVAL) {
282 this.mLastGuiRefresh = SystemClock.elapsedRealtime();
283 mXmppConnectionService.updateConversationUi();
284 }
285 }
286
287 @Override
288 public int getStatus() {
289 return this.mStatus;
290 }
291
292 @Override
293 public long getFileSize() {
294 if (this.file != null) {
295 return this.file.getExpectedSize();
296 } else {
297 return 0;
298 }
299 }
300
301 @Override
302 public int getProgress() {
303 return this.mProgress;
304 }
305
306 @Override
307 public String getMimeType() {
308 return "";
309 }
310}