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