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 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 HttpConnection.this.acceptedAutomatically = false;
150 HttpConnection.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 HttpConnection.this.acceptedAutomatically = true;
163 new Thread(new FileDownloader(interactive)).start();
164 } else {
165 changeStatus(STATUS_OFFER);
166 HttpConnection.this.acceptedAutomatically = false;
167 HttpConnection.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 SSLHandshakeException, 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 = file.createOutputStream();
226 if (os == null) {
227 throw new IOException();
228 }
229 long transmitted = 0;
230 long expected = file.getExpectedSize();
231 int count = -1;
232 byte[] buffer = new byte[1024];
233 while ((count = is.read(buffer)) != -1) {
234 transmitted += count;
235 os.write(buffer, 0, count);
236 updateProgress((int) ((((double) transmitted) / expected) * 100));
237 }
238 os.flush();
239 os.close();
240 is.close();
241 }
242
243 private void updateImageBounds() {
244 message.setType(Message.TYPE_FILE);
245 mXmppConnectionService.getFileBackend().updateFileParams(message, mUrl);
246 mXmppConnectionService.updateMessage(message);
247 }
248
249 }
250
251 public void updateProgress(int i) {
252 this.mProgress = i;
253 if (SystemClock.elapsedRealtime() - this.mLastGuiRefresh > Config.PROGRESS_UI_UPDATE_INTERVAL) {
254 this.mLastGuiRefresh = SystemClock.elapsedRealtime();
255 mXmppConnectionService.updateConversationUi();
256 }
257 }
258
259 @Override
260 public int getStatus() {
261 return this.mStatus;
262 }
263
264 @Override
265 public long getFileSize() {
266 if (this.file != null) {
267 return this.file.getExpectedSize();
268 } else {
269 return 0;
270 }
271 }
272
273 @Override
274 public int getProgress() {
275 return this.mProgress;
276 }
277}