diff options
Diffstat (limited to 'src/main/java/eu/siacs/conversations/http')
-rw-r--r-- | src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java | 92 | ||||
-rw-r--r-- | src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java | 376 |
2 files changed, 468 insertions, 0 deletions
diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java new file mode 100644 index 00000000..e3398f93 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java @@ -0,0 +1,92 @@ +package eu.siacs.conversations.http; + +import org.apache.http.conn.ssl.StrictHostnameVerifier; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.X509TrustManager; + +import de.thedevstack.conversationsplus.ConversationsPlusApplication; + +import de.thedevstack.conversationsplus.utils.MessageUtil; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.AbstractConnectionManager; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.utils.SSLSocketHelper; + +public class HttpConnectionManager extends AbstractConnectionManager { + private static HttpConnectionManager INSTANCE; + + public static void init() { + INSTANCE = new HttpConnectionManager(); + } + + private List<HttpDownloadConnection> downloadConnections = new CopyOnWriteArrayList<>(); + + public static HttpDownloadConnection createNewDownloadConnection(Message message) { + return createNewDownloadConnection(message, false); + } + + public static HttpDownloadConnection createNewDownloadConnection(Message message, boolean interactive) { + if (MessageUtil.needsDownload(message)) { + HttpDownloadConnection connection = new HttpDownloadConnection(INSTANCE); + connection.init(message, interactive); + INSTANCE.downloadConnections.add(connection); + return connection; + } + return null; + } + + public void finishConnection(HttpDownloadConnection connection) { + this.downloadConnections.remove(connection); + } + + public static void setupTrustManager(final HttpsURLConnection connection, final boolean interactive) { + final X509TrustManager trustManager; + final HostnameVerifier hostnameVerifier; + if (interactive) { + trustManager = ConversationsPlusApplication.getMemorizingTrustManager(); + hostnameVerifier = ConversationsPlusApplication.getMemorizingTrustManager().wrapHostnameVerifier( + new StrictHostnameVerifier()); + } else { + trustManager = ConversationsPlusApplication.getMemorizingTrustManager() + .getNonInteractive(); + hostnameVerifier = ConversationsPlusApplication.getMemorizingTrustManager() + .wrapHostnameVerifierNonInteractive( + new StrictHostnameVerifier()); + } + try { + final SSLContext sc = SSLSocketHelper.getSSLContext(); + sc.init(null, new X509TrustManager[]{trustManager}, + ConversationsPlusApplication.getSecureRandom()); + + final SSLSocketFactory sf = sc.getSocketFactory(); + final String[] cipherSuites = CryptoHelper.getOrderedCipherSuites( + sf.getSupportedCipherSuites()); + if (cipherSuites.length > 0) { + sc.getDefaultSSLParameters().setCipherSuites(cipherSuites); + + } + + connection.setSSLSocketFactory(sf); + connection.setHostnameVerifier(hostnameVerifier); + } catch (final KeyManagementException | NoSuchAlgorithmException ignored) { + } + } + + public Proxy getProxy() throws IOException { + return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(InetAddress.getLocalHost(), 8118)); + } +} diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java new file mode 100644 index 00000000..77b8e333 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java @@ -0,0 +1,376 @@ +package eu.siacs.conversations.http; + +import android.os.PowerManager; +import android.util.Log; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.concurrent.CancellationException; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLHandshakeException; + +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.entities.FileParams; +import de.thedevstack.conversationsplus.enums.FileStatus; +import de.thedevstack.conversationsplus.exceptions.RemoteFileNotFoundException; +import de.thedevstack.conversationsplus.utils.MessageUtil; +import de.thedevstack.conversationsplus.utils.StreamUtil; +import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.Transferable; +import eu.siacs.conversations.entities.TransferablePlaceholder; +import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.services.AbstractConnectionManager; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.utils.FileUtils; + +public class HttpDownloadConnection implements Transferable { + + private HttpConnectionManager mHttpConnectionManager; + private XmppConnectionService mXmppConnectionService; + + private URL mUrl; + private Message message; + private DownloadableFile file; + private int mStatus = Transferable.STATUS_UNKNOWN; + private boolean acceptedAutomatically = false; + private int mProgress = 0; + private boolean canceled = false; + + public HttpDownloadConnection(HttpConnectionManager manager) { + this.mHttpConnectionManager = manager; + this.mXmppConnectionService = XmppConnectionServiceAccessor.xmppConnectionService; + } + + @Override + public boolean start() { + if (mXmppConnectionService.hasInternetConnection()) { + if (this.mStatus == STATUS_OFFER_CHECK_FILESIZE) { + checkFileSize(true); + } else { + new Thread(new FileDownloader(true)).start(); + } + return true; + } else { + return false; + } + } + + public void init(Message message) { + init(message, false); + } + + public void init(Message message, boolean interactive) { + this.message = message; + this.message.setTransferable(this); + try { + String url = (null != message && null != message.getFileParams()) ? message.getFileParams().getUrl() : null; + if (null == url) { + /* + * If this code is reached and the URL is null something went wrong. + * Try again to extract the file parameters from the message. + */ + MessageUtil.extractFileParamsFromBody(message); + url = (null != message.getFileParams()) ? message.getFileParams().getUrl() : null; + if (null == url) { + message.setTreatAsDownloadable(Message.Decision.NEVER); // TODO find sth better + this.cancel(); + return; + } + } + mUrl = new URL(url); + final String sUrlFilename = mUrl.getPath().substring(mUrl.getPath().lastIndexOf('/') + 1).toLowerCase(); + final String lastPart = FileUtils.getLastExtension(sUrlFilename); + + if (!lastPart.isEmpty() && ("pgp".equals(lastPart) || "gpg".equals(lastPart))) { + this.message.setEncryption(Message.ENCRYPTION_PGP); + } else if (message.getEncryption() != Message.ENCRYPTION_OTR + && message.getEncryption() != Message.ENCRYPTION_AXOLOTL) { + this.message.setEncryption(Message.ENCRYPTION_NONE); + } + + String extension; + String originalFilename; + if (!lastPart.isEmpty() && VALID_CRYPTO_EXTENSIONS.contains(lastPart)) { + extension = FileUtils.getSecondToLastExtension(sUrlFilename); + originalFilename = sUrlFilename.replace("." + lastPart, ""); + } else { + extension = lastPart; + originalFilename = sUrlFilename; + } + message.setRelativeFilePath(message.getUuid() + "." + extension); + this.file = FileBackend.getFile(message, false); + + FileParams fileParams = message.getFileParams(); + if (null == fileParams) { + fileParams = new FileParams(); + message.setFileParams(fileParams); + } + fileParams.setOriginalFilename(originalFilename); + + if ((this.message.getEncryption() == Message.ENCRYPTION_OTR + || this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL) + && this.file.getKey() == null) { + this.message.setEncryption(Message.ENCRYPTION_NONE); + } + checkFileSize(interactive); + } catch (MalformedURLException e) { + this.cancel(); + } + } + + private void checkFileSize(boolean interactive) { + new Thread(new FileSizeChecker(interactive)).start(); + } + + @Override + public void cancel() { + this.canceled = true; + mHttpConnectionManager.finishConnection(this); + if (message.isFileOrImage()) { + message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED)); + } else { + message.setTransferable(null); + } + mXmppConnectionService.updateConversationUi(); + } + + private void finish() { + FileBackend.updateMediaScanner(file, mXmppConnectionService); + message.setTransferable(null); + MessageUtil.setAndSaveFileStatus(this.message, FileStatus.DOWNLOADED); + mHttpConnectionManager.finishConnection(this); + if (message.getEncryption() == Message.ENCRYPTION_PGP) { + message.getConversation().getAccount().getPgpDecryptionService().add(message); + } + mXmppConnectionService.updateConversationUi(); + if (acceptedAutomatically) { + mXmppConnectionService.getNotificationService().push(message); + } + } + + private void changeStatus(int status) { + this.mStatus = status; + mXmppConnectionService.updateConversationUi(); + } + + private void showToastForException(Exception e) { + e.printStackTrace(); + if (e instanceof java.net.UnknownHostException) { + mXmppConnectionService.showErrorToastInUi(R.string.download_failed_server_not_found); + } else if (e instanceof java.net.ConnectException) { + mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_connect); + } else if (!(e instanceof CancellationException)) { + mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found); + } + } + + private class FileSizeChecker implements Runnable { + + private boolean interactive = false; + + public FileSizeChecker(boolean interactive) { + this.interactive = interactive; + } + + @Override + public void run() { + long size; + try { + size = retrieveFileSize(); + } catch (SSLHandshakeException e) { + changeStatus(STATUS_OFFER_CHECK_FILESIZE); + HttpDownloadConnection.this.acceptedAutomatically = false; + HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message); + return; + } catch (RemoteFileNotFoundException e) { + message.setNoDownloadable(); // TODO Set remote file status to not-available + cancel(); + return; + } catch (IOException e) { + Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage()); + if (interactive) { + showToastForException(e); + } + cancel(); + return; + } + file.setExpectedSize(size); + if (mHttpConnectionManager.hasStoragePermission() + && size != -1 + && size <= ConversationsPlusPreferences.autoAcceptFileSize() + && mXmppConnectionService.isDownloadAllowedInConnection()) { + HttpDownloadConnection.this.acceptedAutomatically = true; + new Thread(new FileDownloader(interactive)).start(); + } else { + changeStatus(STATUS_OFFER); + HttpDownloadConnection.this.acceptedAutomatically = false; + HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message); + } + } + + private long retrieveFileSize() throws IOException { + try { + Logging.d(Config.LOGTAG, "retrieve file size. interactive:" + String.valueOf(interactive)); + changeStatus(STATUS_CHECKING); + HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection(); + connection.setRequestMethod("HEAD"); + Logging.d(Config.LOGTAG, "url: " + connection.getURL().toString()); + Logging.d(Config.LOGTAG, "connection: " + connection.toString()); + connection.setRequestProperty("User-Agent", ConversationsPlusApplication.getNameAndVersion()); + // https://code.google.com/p/android/issues/detail?id=24672 + connection.setRequestProperty("Accept-Encoding", ""); + if (connection instanceof HttpsURLConnection) { + mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive); + } + connection.connect(); + if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) { + Logging.d(Config.LOGTAG, "remote file not found"); + throw new RemoteFileNotFoundException(); + } + String contentLength = connection.getHeaderField("Content-Length"); + connection.disconnect(); + if (contentLength == null) { + return -1; + } + return Long.parseLong(contentLength, 10); + } catch (RemoteFileNotFoundException e) { + throw e; + } catch (IOException e) { + return -1; + } catch (NumberFormatException e) { + return -1; + } + } + + } + + private class FileDownloader implements Runnable { + + private boolean interactive = false; + + private OutputStream os; + + public FileDownloader(boolean interactive) { + this.interactive = interactive; + } + + @Override + public void run() { + try { + changeStatus(STATUS_DOWNLOADING); + download(); + updateImageBounds(); + finish(); + } catch (SSLHandshakeException e) { + changeStatus(STATUS_OFFER); + } catch (Exception e) { + if (interactive) { + showToastForException(e); + } + cancel(); + } + } + + private void download() throws SSLHandshakeException, IOException { + InputStream is = null; + PowerManager.WakeLock wakeLock = ConversationsPlusApplication.createPartialWakeLock("http_download_"+message.getUuid()); + try { + wakeLock.acquire(); + HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection(); + if (connection instanceof HttpsURLConnection) { + mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive); + } + connection.setRequestProperty("User-Agent", ConversationsPlusApplication.getNameAndVersion()); + final boolean tryResume = file.exists() && file.getKey() == null; + if (tryResume) { + Logging.d(Config.LOGTAG, "http download trying resume"); + long size = file.getSize(); + connection.setRequestProperty("Range", "bytes="+size+"-"); + } + connection.connect(); + is = new BufferedInputStream(connection.getInputStream()); + boolean serverResumed = "bytes".equals(connection.getHeaderField("Accept-Ranges")); + long transmitted = 0; + long expected = file.getExpectedSize(); + if (tryResume && serverResumed) { + Logging.d(Config.LOGTAG, "server resumed"); + transmitted = file.getSize(); + updateProgress((int) ((((double) transmitted) / expected) * 100)); + os = AbstractConnectionManager.createAppendedOutputStream(file); + } else { + file.getParentFile().mkdirs(); + file.createNewFile(); + os = AbstractConnectionManager.createOutputStream(file, true); + } + int count = -1; + byte[] buffer = new byte[1024]; + while ((count = is.read(buffer)) != -1) { + transmitted += count; + os.write(buffer, 0, count); + updateProgress((int) ((((double) transmitted) / expected) * 100)); + if (canceled) { + throw new CancellationException(); + } + } + } catch (CancellationException | IOException e) { + throw e; + } finally { + if (os != null) { + try { + os.flush(); + } catch (final IOException ignored) { + + } + } + StreamUtil.close(os); + StreamUtil.close(is); + wakeLock.release(); + } + } + + private void updateImageBounds() { + message.setType(Message.TYPE_FILE); + MessageUtil.updateFileParams(message, mUrl); + mXmppConnectionService.updateMessage(message); + } + + } + + public void updateProgress(int i) { + this.mProgress = i; + mXmppConnectionService.updateConversationUi(); + } + + @Override + public int getStatus() { + return this.mStatus; + } + + @Override + public long getFileSize() { + if (this.file != null) { + return this.file.getExpectedSize(); + } else { + return 0; + } + } + + @Override + public int getProgress() { + return this.mProgress; + } +} |