1package eu.siacs.conversations.http;
2
3import android.content.Intent;
4import android.net.Uri;
5import android.util.Log;
6
7import org.bouncycastle.crypto.engines.AESEngine;
8import org.bouncycastle.crypto.io.CipherOutputStream;
9import org.bouncycastle.crypto.modes.AEADBlockCipher;
10import org.bouncycastle.crypto.modes.GCMBlockCipher;
11import org.bouncycastle.crypto.params.AEADParameters;
12import org.bouncycastle.crypto.params.KeyParameter;
13
14import java.io.BufferedInputStream;
15import java.io.FileOutputStream;
16import java.io.IOException;
17import java.io.OutputStream;
18import java.net.HttpURLConnection;
19import java.net.MalformedURLException;
20import java.net.URL;
21import java.util.Arrays;
22
23import javax.net.ssl.HttpsURLConnection;
24import javax.net.ssl.SSLHandshakeException;
25
26import eu.siacs.conversations.Config;
27import eu.siacs.conversations.R;
28import eu.siacs.conversations.entities.DownloadableFile;
29import eu.siacs.conversations.entities.Message;
30import eu.siacs.conversations.entities.Transferable;
31import eu.siacs.conversations.services.XmppConnectionService;
32import eu.siacs.conversations.utils.CryptoHelper;
33
34public class HttpDownloadConnection implements Transferable {
35
36 private HttpConnectionManager mHttpConnectionManager;
37 private XmppConnectionService mXmppConnectionService;
38
39 private URL mUrl;
40 private Message message;
41 private DownloadableFile file;
42 private int mStatus = Transferable.STATUS_UNKNOWN;
43 private boolean acceptedAutomatically = false;
44 private int mProgress = 0;
45
46 public HttpDownloadConnection(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 init(message, false);
67 }
68
69 public void init(Message message, boolean interactive) {
70 this.message = message;
71 this.message.setTransferable(this);
72 try {
73 mUrl = new URL(message.getBody());
74 String[] parts = mUrl.getPath().toLowerCase().split("\\.");
75 String lastPart = parts.length >= 1 ? parts[parts.length - 1] : null;
76 String secondToLast = parts.length >= 2 ? parts[parts.length -2] : null;
77 if ("pgp".equals(lastPart) || "gpg".equals(lastPart)) {
78 this.message.setEncryption(Message.ENCRYPTION_PGP);
79 } else if (message.getEncryption() != Message.ENCRYPTION_OTR
80 && message.getEncryption() != Message.ENCRYPTION_AXOLOTL) {
81 this.message.setEncryption(Message.ENCRYPTION_NONE);
82 }
83 String extension;
84 if (Arrays.asList(VALID_CRYPTO_EXTENSIONS).contains(lastPart)) {
85 extension = secondToLast;
86 } else {
87 extension = lastPart;
88 }
89 message.setRelativeFilePath(message.getUuid()+"."+extension);
90 this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
91 String reference = mUrl.getRef();
92 if (reference != null && reference.length() == 96) {
93 this.file.setKey(CryptoHelper.hexToBytes(reference));
94 }
95
96 if ((this.message.getEncryption() == Message.ENCRYPTION_OTR
97 || this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL)
98 && this.file.getKey() == null) {
99 this.message.setEncryption(Message.ENCRYPTION_NONE);
100 }
101 checkFileSize(interactive);
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.setTransferable(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.setTransferable(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 class FileSizeChecker implements Runnable {
135
136 private boolean interactive = false;
137
138 public FileSizeChecker(boolean interactive) {
139 this.interactive = interactive;
140 }
141
142 @Override
143 public void run() {
144 long size;
145 try {
146 size = retrieveFileSize();
147 } catch (SSLHandshakeException e) {
148 changeStatus(STATUS_OFFER_CHECK_FILESIZE);
149 HttpDownloadConnection.this.acceptedAutomatically = false;
150 HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
151 return;
152 } catch (IOException e) {
153 Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage());
154 if (interactive) {
155 mXmppConnectionService.showErrorToastInUi(R.string.file_not_found_on_remote_host);
156 }
157 cancel();
158 return;
159 }
160 file.setExpectedSize(size);
161 if (size <= mHttpConnectionManager.getAutoAcceptFileSize()) {
162 HttpDownloadConnection.this.acceptedAutomatically = true;
163 new Thread(new FileDownloader(interactive)).start();
164 } else {
165 changeStatus(STATUS_OFFER);
166 HttpDownloadConnection.this.acceptedAutomatically = false;
167 HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
168 }
169 }
170
171 private long retrieveFileSize() throws IOException {
172 Log.d(Config.LOGTAG,"retrieve file size. interactive:"+String.valueOf(interactive));
173 changeStatus(STATUS_CHECKING);
174 HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection();
175 connection.setRequestMethod("HEAD");
176 if (connection instanceof HttpsURLConnection) {
177 mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
178 }
179 connection.connect();
180 String contentLength = connection.getHeaderField("Content-Length");
181 if (contentLength == null) {
182 throw new IOException();
183 }
184 try {
185 return Long.parseLong(contentLength, 10);
186 } catch (NumberFormatException e) {
187 throw new IOException();
188 }
189 }
190
191 }
192
193 private class FileDownloader implements Runnable {
194
195 private boolean interactive = false;
196
197 public FileDownloader(boolean interactive) {
198 this.interactive = interactive;
199 }
200
201 @Override
202 public void run() {
203 try {
204 changeStatus(STATUS_DOWNLOADING);
205 download();
206 updateImageBounds();
207 finish();
208 } catch (SSLHandshakeException e) {
209 changeStatus(STATUS_OFFER);
210 } catch (IOException e) {
211 mXmppConnectionService.showErrorToastInUi(R.string.file_not_found_on_remote_host);
212 cancel();
213 }
214 }
215
216 private void download() throws IOException {
217 HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection();
218 if (connection instanceof HttpsURLConnection) {
219 mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
220 }
221 connection.connect();
222 BufferedInputStream is = new BufferedInputStream(connection.getInputStream());
223 file.getParentFile().mkdirs();
224 file.createNewFile();
225 OutputStream os;
226 if (file.getKey() != null) {
227 AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
228 cipher.init(false, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
229 os = new CipherOutputStream(new FileOutputStream(file), cipher);
230 } else {
231 os = new FileOutputStream(file);
232 }
233 long transmitted = 0;
234 long expected = file.getExpectedSize();
235 int count = -1;
236 byte[] buffer = new byte[1024];
237 while ((count = is.read(buffer)) != -1) {
238 transmitted += count;
239 os.write(buffer, 0, count);
240 updateProgress((int) ((((double) transmitted) / expected) * 100));
241 }
242 os.flush();
243 os.close();
244 is.close();
245 }
246
247 private void updateImageBounds() {
248 message.setType(Message.TYPE_FILE);
249 mXmppConnectionService.getFileBackend().updateFileParams(message, mUrl);
250 mXmppConnectionService.updateMessage(message);
251 }
252
253 }
254
255 public void updateProgress(int i) {
256 this.mProgress = i;
257 mXmppConnectionService.updateConversationUi();
258 }
259
260 @Override
261 public int getStatus() {
262 return this.mStatus;
263 }
264
265 @Override
266 public long getFileSize() {
267 if (this.file != null) {
268 return this.file.getExpectedSize();
269 } else {
270 return 0;
271 }
272 }
273
274 @Override
275 public int getProgress() {
276 return this.mProgress;
277 }
278}