From c5025039949e94d6418ef6eec87275bda2cb503e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 19 Jan 2020 10:01:43 +0100 Subject: [PATCH] pass omemo decrypt up to higher layers to count as download error. decrypt all encrypted files --- .../http/HttpDownloadConnection.java | 927 +++++++++--------- 1 file changed, 462 insertions(+), 465 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java index 1d88d6ff4..1ee08e244 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java @@ -31,493 +31,490 @@ import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.FileWriterException; import eu.siacs.conversations.utils.WakeLockHelper; -import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.stanzas.IqPacket; import rocks.xmpp.addr.Jid; public class HttpDownloadConnection implements Transferable { - private HttpConnectionManager mHttpConnectionManager; - private XmppConnectionService mXmppConnectionService; + private final Message message; + private final boolean mUseTor; + private HttpConnectionManager mHttpConnectionManager; + private XmppConnectionService mXmppConnectionService; + private URL mUrl; + private DownloadableFile file; + private int mStatus = Transferable.STATUS_UNKNOWN; + private boolean acceptedAutomatically = false; + private int mProgress = 0; + private boolean canceled = false; + private Method method = Method.HTTP_UPLOAD; - private URL mUrl; - private final Message message; - private DownloadableFile file; - private int mStatus = Transferable.STATUS_UNKNOWN; - private boolean acceptedAutomatically = false; - private int mProgress = 0; - private final boolean mUseTor; - private boolean canceled = false; - private Method method = Method.HTTP_UPLOAD; + HttpDownloadConnection(Message message, HttpConnectionManager manager) { + this.message = message; + this.mHttpConnectionManager = manager; + this.mXmppConnectionService = manager.getXmppConnectionService(); + this.mUseTor = mXmppConnectionService.useTorToConnect(); + } - HttpDownloadConnection(Message message, HttpConnectionManager manager) { - this.message = message; - this.mHttpConnectionManager = manager; - this.mXmppConnectionService = manager.getXmppConnectionService(); - this.mUseTor = mXmppConnectionService.useTorToConnect(); - } + @Override + public boolean start() { + if (mXmppConnectionService.hasInternetConnection()) { + if (this.mStatus == STATUS_OFFER_CHECK_FILESIZE) { + checkFileSize(true); + } else { + download(true); + } + return true; + } else { + return false; + } + } - @Override - public boolean start() { - if (mXmppConnectionService.hasInternetConnection()) { - if (this.mStatus == STATUS_OFFER_CHECK_FILESIZE) { - checkFileSize(true); - } else { - download(true); - } - return true; - } else { - return false; - } - } - - public void init(boolean interactive) { - if (message.isDeleted()) { - if (message.getType() == Message.TYPE_PRIVATE_FILE) { - message.setType(Message.TYPE_PRIVATE); - } else if (message.isFileOrImage()) { - message.setType(Message.TYPE_TEXT); - } - message.setDeleted(false); - mXmppConnectionService.updateMessage(message); - } - this.message.setTransferable(this); - try { - final Message.FileParams fileParams = message.getFileParams(); - if (message.hasFileOnRemoteHost()) { - mUrl = CryptoHelper.toHttpsUrl(fileParams.url); - } else if (message.isOOb() && fileParams.url != null && fileParams.size > 0) { - mUrl = fileParams.url; - } else { - mUrl = CryptoHelper.toHttpsUrl(new URL(message.getBody().split("\n")[0])); - } - final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.getPath()); - if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) { - this.message.setEncryption(Message.ENCRYPTION_PGP); - } else if (message.getEncryption() != Message.ENCRYPTION_OTR - && message.getEncryption() != Message.ENCRYPTION_AXOLOTL) { - this.message.setEncryption(Message.ENCRYPTION_NONE); - } - final String ext; - if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) { - ext = extension.secondary; - } else { - ext = extension.main; - } - message.setRelativeFilePath(message.getUuid() + (ext != null ? ("." + ext) : "")); - if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { - this.file = new DownloadableFile(mXmppConnectionService.getCacheDir().getAbsolutePath() + "/" + message.getUuid()); - Log.d(Config.LOGTAG, "create temporary OMEMO encrypted file: " + this.file.getAbsolutePath() + "(" + message.getMimeType() + ")"); - } else { - this.file = mXmppConnectionService.getFileBackend().getFile(message, false); - } - final String reference = mUrl.getRef(); - if (reference != null && AesGcmURLStreamHandler.IV_KEY.matcher(reference).matches()) { - this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference)); - } - - if (this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL && this.file.getKey() == null) { - this.message.setEncryption(Message.ENCRYPTION_NONE); - } - method = mUrl.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME) ? Method.P1_S3 : Method.HTTP_UPLOAD; - long knownFileSize = message.getFileParams().size; - if (knownFileSize > 0 && interactive && method != Method.P1_S3) { - this.file.setExpectedSize(knownFileSize); - download(true); - } else { - checkFileSize(interactive); - } - } catch (MalformedURLException e) { - this.cancel(); - } - } - - private void download(boolean interactive) { - new Thread(new FileDownloader(interactive)).start(); - } - - private void checkFileSize(boolean interactive) { - new Thread(new FileSizeChecker(interactive)).start(); - } - - @Override - public void cancel() { - this.canceled = true; - mHttpConnectionManager.finishConnection(this); - message.setTransferable(null); - if (message.isFileOrImage()) { - message.setDeleted(true); - } - mHttpConnectionManager.updateConversationUi(true); - } - - private void decryptOmemoFile() { - final DownloadableFile outputFile = mXmppConnectionService.getFileBackend().getFile(message, true); - - if (outputFile.getParentFile().mkdirs()) { - Log.d(Config.LOGTAG, "created parent directories for " + outputFile.getAbsolutePath()); - } - - try { - outputFile.createNewFile(); - final InputStream is = new FileInputStream(this.file); - - outputFile.setKey(this.file.getKey()); - outputFile.setIv(this.file.getIv()); - final OutputStream os = AbstractConnectionManager.createOutputStream(outputFile, false, true); - - ByteStreams.copy(is, os); - - FileBackend.close(is); - FileBackend.close(os); - - if (!file.delete()) { - Log.w(Config.LOGTAG,"unable to delete temporary OMEMO encrypted file " + file.getAbsolutePath()); - } - - message.setRelativeFilePath(outputFile.getPath()); - } catch (IOException e) { - message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED); - mXmppConnectionService.updateMessage(message); - } - } - - private void finish() { - message.setTransferable(null); - mHttpConnectionManager.finishConnection(this); - boolean notify = acceptedAutomatically && !message.isRead(); - if (message.getEncryption() == Message.ENCRYPTION_PGP) { - notify = message.getConversation().getAccount().getPgpDecryptionService().decrypt(message, notify); - } - mHttpConnectionManager.updateConversationUi(true); - final boolean notifyAfterScan = notify; - final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message, true); - mXmppConnectionService.getFileBackend().updateMediaScanner(file, () -> { - if (notifyAfterScan) { - mXmppConnectionService.getNotificationService().push(message); - } - }); - } - - private void decryptIfNeeded() { - if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { - decryptOmemoFile(); - } - } - - private void changeStatus(int status) { - this.mStatus = status; - mHttpConnectionManager.updateConversationUi(true); - } - - private void showToastForException(Exception e) { - 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 FileWriterException) { - mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_write_file); - } else if (!(e instanceof CancellationException)) { - mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found); - } - } - - private void updateProgress(long i) { - this.mProgress = (int) i; - mHttpConnectionManager.updateConversationUi(false); - } - - @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; - } - - public Message getMessage() { - return message; - } - - private class FileSizeChecker implements Runnable { - - private final boolean interactive; - - FileSizeChecker(boolean interactive) { - this.interactive = interactive; - } + public void init(boolean interactive) { + if (message.isDeleted()) { + if (message.getType() == Message.TYPE_PRIVATE_FILE) { + message.setType(Message.TYPE_PRIVATE); + } else if (message.isFileOrImage()) { + message.setType(Message.TYPE_TEXT); + } + message.setDeleted(false); + mXmppConnectionService.updateMessage(message); + } + this.message.setTransferable(this); + try { + final Message.FileParams fileParams = message.getFileParams(); + if (message.hasFileOnRemoteHost()) { + mUrl = CryptoHelper.toHttpsUrl(fileParams.url); + } else if (message.isOOb() && fileParams.url != null && fileParams.size > 0) { + mUrl = fileParams.url; + } else { + mUrl = CryptoHelper.toHttpsUrl(new URL(message.getBody().split("\n")[0])); + } + final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.getPath()); + if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) { + this.message.setEncryption(Message.ENCRYPTION_PGP); + } else if (message.getEncryption() != Message.ENCRYPTION_OTR + && message.getEncryption() != Message.ENCRYPTION_AXOLOTL) { + this.message.setEncryption(Message.ENCRYPTION_NONE); + } + final String ext; + if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) { + ext = extension.secondary; + } else { + ext = extension.main; + } + message.setRelativeFilePath(message.getUuid() + (ext != null ? ("." + ext) : "")); + final String reference = mUrl.getRef(); + if (reference != null && AesGcmURLStreamHandler.IV_KEY.matcher(reference).matches()) { + this.file = new DownloadableFile(mXmppConnectionService.getCacheDir().getAbsolutePath() + "/" + message.getUuid()); + this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference)); + Log.d(Config.LOGTAG, "create temporary OMEMO encrypted file: " + this.file.getAbsolutePath() + "(" + message.getMimeType() + ")"); + } else { + this.file = mXmppConnectionService.getFileBackend().getFile(message, false); + } - @Override - public void run() { - if (mUrl.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME)) { - retrieveUrl(); - } else { - check(); - } - } + if (this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL && this.file.getKey() == null) { + this.message.setEncryption(Message.ENCRYPTION_NONE); + } + method = mUrl.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME) ? Method.P1_S3 : Method.HTTP_UPLOAD; + long knownFileSize = message.getFileParams().size; + if (knownFileSize > 0 && interactive && method != Method.P1_S3) { + this.file.setExpectedSize(knownFileSize); + download(true); + } else { + checkFileSize(interactive); + } + } catch (MalformedURLException e) { + this.cancel(); + } + } - private void retrieveUrl() { - changeStatus(STATUS_CHECKING); - final Account account = message.getConversation().getAccount(); - IqPacket request = mXmppConnectionService.getIqGenerator().requestP1S3Url(Jid.of(account.getJid().getDomain()), mUrl.getHost()); - mXmppConnectionService.sendIqPacket(message.getConversation().getAccount(), request, (a, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - String download = packet.query().getAttribute("download"); - if (download != null) { - try { - mUrl = new URL(download); - check(); - return; - } catch (MalformedURLException e) { - //fallthrough - } - } - } - Log.d(Config.LOGTAG,"unable to retrieve actual download url"); - retrieveFailed(null); - }); - } + private void download(boolean interactive) { + new Thread(new FileDownloader(interactive)).start(); + } - private void retrieveFailed(@Nullable Exception e) { - changeStatus(STATUS_OFFER_CHECK_FILESIZE); - if (interactive) { - if (e != null) { - showToastForException(e); - } - } else { - HttpDownloadConnection.this.acceptedAutomatically = false; - HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message); - } - cancel(); - } + private void checkFileSize(boolean interactive) { + new Thread(new FileSizeChecker(interactive)).start(); + } - private void check() { - long size; - try { - size = retrieveFileSize(); - } catch (Exception e) { - Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage()); - retrieveFailed(e); - return; - } - final Message.FileParams fileParams = message.getFileParams(); - FileBackend.updateFileParams(message, fileParams.url, size); - message.setOob(true); - mXmppConnectionService.databaseBackend.updateMessage(message, true); - file.setExpectedSize(size); - message.resetFileParams(); - if (mHttpConnectionManager.hasStoragePermission() - && size <= mHttpConnectionManager.getAutoAcceptFileSize() - && mXmppConnectionService.isDataSaverDisabled()) { - HttpDownloadConnection.this.acceptedAutomatically = true; - download(interactive); - } else { - changeStatus(STATUS_OFFER); - HttpDownloadConnection.this.acceptedAutomatically = false; - HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message); - } - } + @Override + public void cancel() { + this.canceled = true; + mHttpConnectionManager.finishConnection(this); + message.setTransferable(null); + if (message.isFileOrImage()) { + message.setDeleted(true); + } + mHttpConnectionManager.updateConversationUi(true); + } - private long retrieveFileSize() throws IOException { - try { - Log.d(Config.LOGTAG, "retrieve file size. interactive:" + String.valueOf(interactive)); - changeStatus(STATUS_CHECKING); - HttpURLConnection connection; - final String hostname = mUrl.getHost(); - final boolean onion = hostname != null && hostname.endsWith(".onion"); - if (mUseTor || message.getConversation().getAccount().isOnion() || onion) { - connection = (HttpURLConnection) mUrl.openConnection(HttpConnectionManager.getProxy()); - } else { - connection = (HttpURLConnection) mUrl.openConnection(); - } - if (method == Method.P1_S3) { - connection.setRequestMethod("GET"); - connection.addRequestProperty("Range","bytes=0-0"); - } else { - connection.setRequestMethod("HEAD"); - } - connection.setUseCaches(false); - Log.d(Config.LOGTAG, "url: " + connection.getURL().toString()); - connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getUserAgent()); - if (connection instanceof HttpsURLConnection) { - mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive); - } - connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000); - connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000); - connection.connect(); - String contentLength; - if (method == Method.P1_S3) { - String contentRange = connection.getHeaderField("Content-Range"); - String[] contentRangeParts = contentRange == null ? new String[0] : contentRange.split("/"); - if (contentRangeParts.length != 2) { - contentLength = null; - } else { - contentLength = contentRangeParts[1]; - } - } else { - contentLength = connection.getHeaderField("Content-Length"); - } - connection.disconnect(); - if (contentLength == null) { - throw new IOException("no content-length found in HEAD response"); - } - return Long.parseLong(contentLength, 10); - } catch (IOException e) { - Log.d(Config.LOGTAG, "io exception during HEAD " + e.getMessage()); - throw e; - } catch (NumberFormatException e) { - throw new IOException(); - } - } + private void decryptFile() throws IOException { + final DownloadableFile outputFile = mXmppConnectionService.getFileBackend().getFile(message, true); - } + if (outputFile.getParentFile().mkdirs()) { + Log.d(Config.LOGTAG, "created parent directories for " + outputFile.getAbsolutePath()); + } - private class FileDownloader implements Runnable { + if (!outputFile.createNewFile()) { + Log.w(Config.LOGTAG, "unable to create output file " + outputFile.getAbsolutePath()); + } - private final boolean interactive; + final InputStream is = new FileInputStream(this.file); - private OutputStream os; + outputFile.setKey(this.file.getKey()); + outputFile.setIv(this.file.getIv()); + final OutputStream os = AbstractConnectionManager.createOutputStream(outputFile, false, true); - public FileDownloader(boolean interactive) { - this.interactive = interactive; - } + ByteStreams.copy(is, os); - @Override - public void run() { - try { - changeStatus(STATUS_DOWNLOADING); - download(); - decryptIfNeeded(); - updateImageBounds(); - finish(); - } catch (SSLHandshakeException e) { - changeStatus(STATUS_OFFER); - } catch (Exception e) { - if (interactive) { - showToastForException(e); - } else { - HttpDownloadConnection.this.acceptedAutomatically = false; - HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message); - } - cancel(); - } - } + FileBackend.close(is); + FileBackend.close(os); - private void download() throws Exception { - InputStream is = null; - HttpURLConnection connection = null; - PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_download_" + message.getUuid()); - try { - wakeLock.acquire(); - if (mUseTor || message.getConversation().getAccount().isOnion()) { - connection = (HttpURLConnection) mUrl.openConnection(HttpConnectionManager.getProxy()); - } else { - connection = (HttpURLConnection) mUrl.openConnection(); - } - if (connection instanceof HttpsURLConnection) { - mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive); - } - connection.setUseCaches(false); - connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getUserAgent()); - final long expected = file.getExpectedSize(); - final boolean tryResume = file.exists() && file.getSize() > 0 && file.getSize() < expected; - long resumeSize = 0; + if (!file.delete()) { + Log.w(Config.LOGTAG, "unable to delete temporary OMEMO encrypted file " + file.getAbsolutePath()); + } - if (tryResume) { - resumeSize = file.getSize(); - Log.d(Config.LOGTAG, "http download trying resume after " + resumeSize + " of " + expected); - connection.setRequestProperty("Range", "bytes=" + resumeSize + "-"); - } - connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000); - connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000); - connection.connect(); - is = new BufferedInputStream(connection.getInputStream()); - final String contentRange = connection.getHeaderField("Content-Range"); - boolean serverResumed = tryResume && contentRange != null && contentRange.startsWith("bytes " + resumeSize + "-"); - long transmitted = 0; - if (tryResume && serverResumed) { - Log.d(Config.LOGTAG, "server resumed"); - transmitted = file.getSize(); - updateProgress(Math.round(((double) transmitted / expected) * 100)); - os = AbstractConnectionManager.createOutputStream(file, true, false); - if (os == null) { - throw new FileWriterException(); - } - } else { - long reportedContentLengthOnGet; - try { - reportedContentLengthOnGet = Long.parseLong(connection.getHeaderField("Content-Length")); - } catch (NumberFormatException | NullPointerException e) { - reportedContentLengthOnGet = 0; - } - if (expected != reportedContentLengthOnGet) { - Log.d(Config.LOGTAG, "content-length reported on GET (" + reportedContentLengthOnGet + ") did not match Content-Length reported on HEAD (" + expected + ")"); - } - file.getParentFile().mkdirs(); - if (!file.exists() && !file.createNewFile()) { - throw new FileWriterException(); - } - os = AbstractConnectionManager.createOutputStream(file, false, false); - } - int count; - byte[] buffer = new byte[4096]; - while ((count = is.read(buffer)) != -1) { - transmitted += count; - try { - os.write(buffer, 0, count); - } catch (IOException e) { - throw new FileWriterException(); - } - updateProgress(Math.round(((double) transmitted / expected) * 100)); - if (canceled) { - throw new CancellationException(); - } - } - try { - os.flush(); - } catch (IOException e) { - throw new FileWriterException(); - } - } catch (CancellationException | IOException e) { - Log.d(Config.LOGTAG, "http download failed " + e.getMessage()); - throw e; - } finally { - FileBackend.close(os); - FileBackend.close(is); - if (connection != null) { - connection.disconnect(); - } - WakeLockHelper.release(wakeLock); - } - } + //TODO can this be skipped? + //message.setRelativeFilePath(outputFile.getPath()); - private void updateImageBounds() { - final boolean privateMessage = message.isPrivateMessage(); - message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : Message.TYPE_FILE); - final URL url; - final String ref = mUrl.getRef(); - if (method == Method.P1_S3) { - url = message.getFileParams().url; - } else if (ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches()) { - url = CryptoHelper.toAesGcmUrl(mUrl); - } else { - url = mUrl; - } - mXmppConnectionService.getFileBackend().updateFileParams(message, url); - mXmppConnectionService.updateMessage(message); - } + } - } + private void finish() { + message.setTransferable(null); + mHttpConnectionManager.finishConnection(this); + boolean notify = acceptedAutomatically && !message.isRead(); + if (message.getEncryption() == Message.ENCRYPTION_PGP) { + notify = message.getConversation().getAccount().getPgpDecryptionService().decrypt(message, notify); + } + mHttpConnectionManager.updateConversationUi(true); + final boolean notifyAfterScan = notify; + final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message, true); + mXmppConnectionService.getFileBackend().updateMediaScanner(file, () -> { + if (notifyAfterScan) { + mXmppConnectionService.getNotificationService().push(message); + } + }); + } + + private void decryptIfNeeded() throws IOException { + if (file.getKey() != null && file.getIv() != null) { + decryptFile(); + } + } + + private void changeStatus(int status) { + this.mStatus = status; + mHttpConnectionManager.updateConversationUi(true); + } + + private void showToastForException(Exception e) { + 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 FileWriterException) { + mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_write_file); + } else if (!(e instanceof CancellationException)) { + mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found); + } + } + + private void updateProgress(long i) { + this.mProgress = (int) i; + mHttpConnectionManager.updateConversationUi(false); + } + + @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; + } + + public Message getMessage() { + return message; + } + + private class FileSizeChecker implements Runnable { + + private final boolean interactive; + + FileSizeChecker(boolean interactive) { + this.interactive = interactive; + } + + + @Override + public void run() { + if (mUrl.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME)) { + retrieveUrl(); + } else { + check(); + } + } + + private void retrieveUrl() { + changeStatus(STATUS_CHECKING); + final Account account = message.getConversation().getAccount(); + IqPacket request = mXmppConnectionService.getIqGenerator().requestP1S3Url(Jid.of(account.getJid().getDomain()), mUrl.getHost()); + mXmppConnectionService.sendIqPacket(message.getConversation().getAccount(), request, (a, packet) -> { + if (packet.getType() == IqPacket.TYPE.RESULT) { + String download = packet.query().getAttribute("download"); + if (download != null) { + try { + mUrl = new URL(download); + check(); + return; + } catch (MalformedURLException e) { + //fallthrough + } + } + } + Log.d(Config.LOGTAG, "unable to retrieve actual download url"); + retrieveFailed(null); + }); + } + + private void retrieveFailed(@Nullable Exception e) { + changeStatus(STATUS_OFFER_CHECK_FILESIZE); + if (interactive) { + if (e != null) { + showToastForException(e); + } + } else { + HttpDownloadConnection.this.acceptedAutomatically = false; + HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message); + } + cancel(); + } + + private void check() { + long size; + try { + size = retrieveFileSize(); + } catch (Exception e) { + Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage()); + retrieveFailed(e); + return; + } + final Message.FileParams fileParams = message.getFileParams(); + FileBackend.updateFileParams(message, fileParams.url, size); + message.setOob(true); + mXmppConnectionService.databaseBackend.updateMessage(message, true); + file.setExpectedSize(size); + message.resetFileParams(); + if (mHttpConnectionManager.hasStoragePermission() + && size <= mHttpConnectionManager.getAutoAcceptFileSize() + && mXmppConnectionService.isDataSaverDisabled()) { + HttpDownloadConnection.this.acceptedAutomatically = true; + download(interactive); + } else { + changeStatus(STATUS_OFFER); + HttpDownloadConnection.this.acceptedAutomatically = false; + HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message); + } + } + + private long retrieveFileSize() throws IOException { + try { + Log.d(Config.LOGTAG, "retrieve file size. interactive:" + String.valueOf(interactive)); + changeStatus(STATUS_CHECKING); + HttpURLConnection connection; + final String hostname = mUrl.getHost(); + final boolean onion = hostname != null && hostname.endsWith(".onion"); + if (mUseTor || message.getConversation().getAccount().isOnion() || onion) { + connection = (HttpURLConnection) mUrl.openConnection(HttpConnectionManager.getProxy()); + } else { + connection = (HttpURLConnection) mUrl.openConnection(); + } + if (method == Method.P1_S3) { + connection.setRequestMethod("GET"); + connection.addRequestProperty("Range", "bytes=0-0"); + } else { + connection.setRequestMethod("HEAD"); + } + connection.setUseCaches(false); + Log.d(Config.LOGTAG, "url: " + connection.getURL().toString()); + connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getUserAgent()); + if (connection instanceof HttpsURLConnection) { + mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive); + } + connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000); + connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000); + connection.connect(); + String contentLength; + if (method == Method.P1_S3) { + String contentRange = connection.getHeaderField("Content-Range"); + String[] contentRangeParts = contentRange == null ? new String[0] : contentRange.split("/"); + if (contentRangeParts.length != 2) { + contentLength = null; + } else { + contentLength = contentRangeParts[1]; + } + } else { + contentLength = connection.getHeaderField("Content-Length"); + } + connection.disconnect(); + if (contentLength == null) { + throw new IOException("no content-length found in HEAD response"); + } + return Long.parseLong(contentLength, 10); + } catch (IOException e) { + Log.d(Config.LOGTAG, "io exception during HEAD " + e.getMessage()); + throw e; + } catch (NumberFormatException e) { + throw new IOException(); + } + } + + } + + private class FileDownloader implements Runnable { + + private final boolean interactive; + + private OutputStream os; + + public FileDownloader(boolean interactive) { + this.interactive = interactive; + } + + @Override + public void run() { + try { + changeStatus(STATUS_DOWNLOADING); + download(); + decryptIfNeeded(); + updateImageBounds(); + finish(); + } catch (SSLHandshakeException e) { + changeStatus(STATUS_OFFER); + } catch (Exception e) { + if (interactive) { + showToastForException(e); + } else { + HttpDownloadConnection.this.acceptedAutomatically = false; + HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message); + } + cancel(); + } + } + + private void download() throws Exception { + InputStream is = null; + HttpURLConnection connection = null; + PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_download_" + message.getUuid()); + try { + wakeLock.acquire(); + if (mUseTor || message.getConversation().getAccount().isOnion()) { + connection = (HttpURLConnection) mUrl.openConnection(HttpConnectionManager.getProxy()); + } else { + connection = (HttpURLConnection) mUrl.openConnection(); + } + if (connection instanceof HttpsURLConnection) { + mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive); + } + connection.setUseCaches(false); + connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getUserAgent()); + final long expected = file.getExpectedSize(); + final boolean tryResume = file.exists() && file.getSize() > 0 && file.getSize() < expected; + long resumeSize = 0; + + if (tryResume) { + resumeSize = file.getSize(); + Log.d(Config.LOGTAG, "http download trying resume after " + resumeSize + " of " + expected); + connection.setRequestProperty("Range", "bytes=" + resumeSize + "-"); + } + connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000); + connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000); + connection.connect(); + is = new BufferedInputStream(connection.getInputStream()); + final String contentRange = connection.getHeaderField("Content-Range"); + boolean serverResumed = tryResume && contentRange != null && contentRange.startsWith("bytes " + resumeSize + "-"); + long transmitted = 0; + if (tryResume && serverResumed) { + Log.d(Config.LOGTAG, "server resumed"); + transmitted = file.getSize(); + updateProgress(Math.round(((double) transmitted / expected) * 100)); + os = AbstractConnectionManager.createOutputStream(file, true, false); + if (os == null) { + throw new FileWriterException(); + } + } else { + long reportedContentLengthOnGet; + try { + reportedContentLengthOnGet = Long.parseLong(connection.getHeaderField("Content-Length")); + } catch (NumberFormatException | NullPointerException e) { + reportedContentLengthOnGet = 0; + } + if (expected != reportedContentLengthOnGet) { + Log.d(Config.LOGTAG, "content-length reported on GET (" + reportedContentLengthOnGet + ") did not match Content-Length reported on HEAD (" + expected + ")"); + } + file.getParentFile().mkdirs(); + if (!file.exists() && !file.createNewFile()) { + throw new FileWriterException(); + } + os = AbstractConnectionManager.createOutputStream(file, false, false); + } + int count; + byte[] buffer = new byte[4096]; + while ((count = is.read(buffer)) != -1) { + transmitted += count; + try { + os.write(buffer, 0, count); + } catch (IOException e) { + throw new FileWriterException(); + } + updateProgress(Math.round(((double) transmitted / expected) * 100)); + if (canceled) { + throw new CancellationException(); + } + } + try { + os.flush(); + } catch (IOException e) { + throw new FileWriterException(); + } + } catch (CancellationException | IOException e) { + Log.d(Config.LOGTAG, "http download failed " + e.getMessage()); + throw e; + } finally { + FileBackend.close(os); + FileBackend.close(is); + if (connection != null) { + connection.disconnect(); + } + WakeLockHelper.release(wakeLock); + } + } + + private void updateImageBounds() { + final boolean privateMessage = message.isPrivateMessage(); + message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : Message.TYPE_FILE); + final URL url; + final String ref = mUrl.getRef(); + if (method == Method.P1_S3) { + url = message.getFileParams().url; + } else if (ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches()) { + url = CryptoHelper.toAesGcmUrl(mUrl); + } else { + url = mUrl; + } + mXmppConnectionService.getFileBackend().updateFileParams(message, url); + mXmppConnectionService.updateMessage(message); + } + + } }