diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 1c4569baa..e2b7b5afb 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -64,7 +64,7 @@ public class IqGenerator extends AbstractGenerator { query.addChild("version").setContent(getIdentityVersion()); if ("chromium".equals(android.os.Build.BRAND)) { query.addChild("os").setContent("Chrome OS"); - } else{ + } else { query.addChild("os").setContent("Android"); } return packet; @@ -72,7 +72,7 @@ public class IqGenerator extends AbstractGenerator { public IqPacket entityTimeResponse(IqPacket request) { final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT); - Element time = packet.addChild("time","urn:xmpp:time"); + Element time = packet.addChild("time", "urn:xmpp:time"); final long now = System.currentTimeMillis(); time.addChild("utc").setContent(getTimestamp(now)); TimeZone ourTimezone = TimeZone.getDefault(); @@ -80,25 +80,25 @@ public class IqGenerator extends AbstractGenerator { long offsetMinutes = Math.abs((offsetSeconds % 3600) / 60); long offsetHours = offsetSeconds / 3600; String hours; - if (offsetHours<0) { - hours = String.format(Locale.US,"%03d",offsetHours); + if (offsetHours < 0) { + hours = String.format(Locale.US, "%03d", offsetHours); } else { - hours = String.format(Locale.US,"%02d",offsetHours); + hours = String.format(Locale.US, "%02d", offsetHours); } - String minutes = String.format(Locale.US,"%02d",offsetMinutes); - time.addChild("tzo").setContent(hours+":"+minutes); + String minutes = String.format(Locale.US, "%02d", offsetMinutes); + time.addChild("tzo").setContent(hours + ":" + minutes); return packet; } public IqPacket purgeOfflineMessages() { final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - packet.addChild("offline",Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL).addChild("purge"); + packet.addChild("offline", Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL).addChild("purge"); return packet; } protected IqPacket publish(final String node, final Element item, final Bundle options) { final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - final Element pubsub = packet.addChild("pubsub",Namespace.PUBSUB); + final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB); final Element publish = pubsub.addChild("publish"); publish.setAttribute("node", node); publish.addChild(item); @@ -110,12 +110,12 @@ public class IqGenerator extends AbstractGenerator { } protected IqPacket publish(final String node, final Element item) { - return publish(node,item,null); + return publish(node, item, null); } protected IqPacket retrieve(String node, Element item) { final IqPacket packet = new IqPacket(IqPacket.TYPE.GET); - final Element pubsub = packet.addChild("pubsub",Namespace.PUBSUB); + final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB); final Element items = pubsub.addChild("items"); items.setAttribute("node", node); if (item != null) { @@ -126,7 +126,7 @@ public class IqGenerator extends AbstractGenerator { public IqPacket publishNick(String nick) { final Element item = new Element("item"); - item.addChild("nick","http://jabber.org/protocol/nick").setContent(nick); + item.addChild("nick", "http://jabber.org/protocol/nick").setContent(nick); return publish("http://jabber.org/protocol/nick", item); } @@ -142,7 +142,7 @@ public class IqGenerator extends AbstractGenerator { final Element item = new Element("item"); item.setAttribute("id", avatar.sha1sum); final Element metadata = item - .addChild("metadata", "urn:xmpp:avatar:metadata"); + .addChild("metadata", "urn:xmpp:avatar:metadata"); final Element info = metadata.addChild("info"); info.setAttribute("bytes", avatar.size); info.setAttribute("id", avatar.sha1sum); @@ -177,20 +177,20 @@ public class IqGenerator extends AbstractGenerator { public IqPacket retrieveDeviceIds(final Jid to) { final IqPacket packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null); - if(to != null) { + if (to != null) { packet.setTo(to); } return packet; } public IqPacket retrieveBundlesForDevice(final Jid to, final int deviceid) { - final IqPacket packet = retrieve(AxolotlService.PEP_BUNDLES+":"+deviceid, null); + final IqPacket packet = retrieve(AxolotlService.PEP_BUNDLES + ":" + deviceid, null); packet.setTo(to); return packet; } public IqPacket retrieveVerificationForDevice(final Jid to, final int deviceid) { - final IqPacket packet = retrieve(AxolotlService.PEP_VERIFICATION+":"+deviceid, null); + final IqPacket packet = retrieve(AxolotlService.PEP_VERIFICATION + ":" + deviceid, null); packet.setTo(to); return packet; } @@ -198,7 +198,7 @@ public class IqGenerator extends AbstractGenerator { public IqPacket publishDeviceIds(final Set ids, final Bundle publishOptions) { final Element item = new Element("item"); final Element list = item.addChild("list", AxolotlService.PEP_PREFIX); - for(Integer id:ids) { + for (Integer id : ids) { final Element device = new Element("device"); device.setAttribute("id", id); list.addChild(device); @@ -207,43 +207,43 @@ public class IqGenerator extends AbstractGenerator { } public IqPacket publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey, - final Set preKeyRecords, final int deviceId, Bundle publishOptions) { + final Set preKeyRecords, final int deviceId, Bundle publishOptions) { final Element item = new Element("item"); final Element bundle = item.addChild("bundle", AxolotlService.PEP_PREFIX); final Element signedPreKeyPublic = bundle.addChild("signedPreKeyPublic"); signedPreKeyPublic.setAttribute("signedPreKeyId", signedPreKeyRecord.getId()); ECPublicKey publicKey = signedPreKeyRecord.getKeyPair().getPublicKey(); - signedPreKeyPublic.setContent(Base64.encodeToString(publicKey.serialize(),Base64.DEFAULT)); + signedPreKeyPublic.setContent(Base64.encodeToString(publicKey.serialize(), Base64.DEFAULT)); final Element signedPreKeySignature = bundle.addChild("signedPreKeySignature"); - signedPreKeySignature.setContent(Base64.encodeToString(signedPreKeyRecord.getSignature(),Base64.DEFAULT)); + signedPreKeySignature.setContent(Base64.encodeToString(signedPreKeyRecord.getSignature(), Base64.DEFAULT)); final Element identityKeyElement = bundle.addChild("identityKey"); identityKeyElement.setContent(Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT)); final Element prekeys = bundle.addChild("prekeys", AxolotlService.PEP_PREFIX); - for(PreKeyRecord preKeyRecord:preKeyRecords) { + for (PreKeyRecord preKeyRecord : preKeyRecords) { final Element prekey = prekeys.addChild("preKeyPublic"); prekey.setAttribute("preKeyId", preKeyRecord.getId()); prekey.setContent(Base64.encodeToString(preKeyRecord.getKeyPair().getPublicKey().serialize(), Base64.DEFAULT)); } - return publish(AxolotlService.PEP_BUNDLES+":"+deviceId, item, publishOptions); + return publish(AxolotlService.PEP_BUNDLES + ":" + deviceId, item, publishOptions); } public IqPacket publishVerification(byte[] signature, X509Certificate[] certificates, final int deviceId) { final Element item = new Element("item"); final Element verification = item.addChild("verification", AxolotlService.PEP_PREFIX); final Element chain = verification.addChild("chain"); - for(int i = 0; i < certificates.length; ++i) { + for (int i = 0; i < certificates.length; ++i) { try { Element certificate = chain.addChild("certificate"); certificate.setContent(Base64.encodeToString(certificates[i].getEncoded(), Base64.DEFAULT)); - certificate.setAttribute("index",i); + certificate.setAttribute("index", i); } catch (CertificateEncodingException e) { Log.d(Config.LOGTAG, "could not encode certificate"); } } verification.addChild("signature").setContent(Base64.encodeToString(signature, Base64.DEFAULT)); - return publish(AxolotlService.PEP_VERIFICATION+":"+deviceId, item); + return publish(AxolotlService.PEP_VERIFICATION + ":" + deviceId, item); } public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) { @@ -254,7 +254,7 @@ public class IqGenerator extends AbstractGenerator { data.setFormType(mam.isLegacy() ? Namespace.MAM_LEGACY : Namespace.MAM); if (mam.muc()) { packet.setTo(mam.getWith()); - } else if (mam.getWith()!=null) { + } else if (mam.getWith() != null) { data.put("with", mam.getWith().toString()); } final long start = mam.getStart(); @@ -276,6 +276,7 @@ public class IqGenerator extends AbstractGenerator { set.addChild("max").setContent(String.valueOf(Config.PAGE_SIZE)); return packet; } + public IqPacket generateGetBlockList() { final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); iq.addChild("blocklist", Namespace.BLOCKING); @@ -290,7 +291,7 @@ public class IqGenerator extends AbstractGenerator { if (reportSpam) { item.addChild("report", "urn:xmpp:reporting:0").addChild("spam"); } - Log.d(Config.LOGTAG,iq.toString()); + Log.d(Config.LOGTAG, iq.toString()); return iq; } @@ -314,7 +315,7 @@ public class IqGenerator extends AbstractGenerator { public IqPacket changeAffiliation(Conversation conference, Jid jid, String affiliation) { List jids = new ArrayList<>(); jids.add(jid); - return changeAffiliation(conference,jids,affiliation); + return changeAffiliation(conference, jids, affiliation); } public IqPacket changeAffiliation(Conversation conference, List jids, String affiliation) { @@ -322,7 +323,7 @@ public class IqGenerator extends AbstractGenerator { packet.setTo(conference.getJid().asBareJid()); packet.setFrom(conference.getAccount().getJid()); Element query = packet.query("http://jabber.org/protocol/muc#admin"); - for(Jid jid : jids) { + for (Jid jid : jids) { Element item = query.addChild("item"); item.setAttribute("jid", jid.toString()); item.setAttribute("affiliation", affiliation); @@ -344,17 +345,23 @@ public class IqGenerator extends AbstractGenerator { IqPacket packet = new IqPacket(IqPacket.TYPE.GET); packet.setTo(host); Element request = packet.addChild("request", Namespace.HTTP_UPLOAD); - request.setAttribute("filename",convertFilename(file.getName())); - request.setAttribute("size",file.getExpectedSize()); - request.setAttribute("content-type",mime); + request.setAttribute("filename", convertFilename(file.getName())); + request.setAttribute("size", file.getExpectedSize()); + request.setAttribute("content-type", mime); return packet; } public IqPacket requestP1S3Slot(Jid host, String md5) { IqPacket packet = new IqPacket(IqPacket.TYPE.SET); packet.setTo(host); - packet.query(Namespace.P1_S3_FILE_TRANSFER).setAttribute("md5",md5); - Log.d(Config.LOGTAG,packet.toString()); + packet.query(Namespace.P1_S3_FILE_TRANSFER).setAttribute("md5", md5); + return packet; + } + + public IqPacket requestP1S3Url(Jid host, String fileId) { + IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + packet.setTo(host); + packet.query(Namespace.P1_S3_FILE_TRANSFER).setAttribute("fileid", fileId); return packet; } @@ -391,8 +398,8 @@ public class IqGenerator extends AbstractGenerator { IqPacket packet = new IqPacket(IqPacket.TYPE.SET); packet.setTo(appServer); Element command = packet.addChild("command", "http://jabber.org/protocol/commands"); - command.setAttribute("node","register-push-fcm"); - command.setAttribute("action","execute"); + command.setAttribute("node", "register-push-fcm"); + command.setAttribute("action", "execute"); Data data = new Data(); data.put("token", token); data.put("android-id", deviceId); @@ -403,12 +410,12 @@ public class IqGenerator extends AbstractGenerator { public IqPacket enablePush(Jid jid, String node, String secret) { IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - Element enable = packet.addChild("enable","urn:xmpp:push:0"); - enable.setAttribute("jid",jid.toString()); + Element enable = packet.addChild("enable", "urn:xmpp:push:0"); + enable.setAttribute("jid", jid.toString()); enable.setAttribute("node", node); Data data = new Data(); data.setFormType(Namespace.PUBSUB_PUBLISH_OPTIONS); - data.put("secret",secret); + data.put("secret", secret); data.submit(); enable.addChild(data); return packet; @@ -417,7 +424,7 @@ public class IqGenerator extends AbstractGenerator { public IqPacket queryAffiliation(Conversation conversation, String affiliation) { IqPacket packet = new IqPacket(IqPacket.TYPE.GET); packet.setTo(conversation.getJid().asBareJid()); - packet.query("http://jabber.org/protocol/muc#admin").addChild("item").setAttribute("affiliation",affiliation); + packet.query("http://jabber.org/protocol/muc#admin").addChild("item").setAttribute("affiliation", affiliation); return packet; } @@ -427,8 +434,8 @@ public class IqGenerator extends AbstractGenerator { options.putString("muc#roomconfig_membersonly", "1"); options.putString("muc#roomconfig_publicroom", "0"); options.putString("muc#roomconfig_whois", "anyone"); - options.putString("muc#roomconfig_enablearchiving","1"); - options.putString("mam","1"); + options.putString("muc#roomconfig_enablearchiving", "1"); + options.putString("mam", "1"); return options; } @@ -437,14 +444,14 @@ public class IqGenerator extends AbstractGenerator { } public IqPacket publishPubsubConfiguration(Jid jid, String node, Data data) { - return pubsubConfiguration(jid,node,data); + return pubsubConfiguration(jid, node, data); } private IqPacket pubsubConfiguration(Jid jid, String node, Data data) { IqPacket packet = new IqPacket(data == null ? IqPacket.TYPE.GET : IqPacket.TYPE.SET); packet.setTo(jid); - Element pubsub = packet.addChild("pubsub","http://jabber.org/protocol/pubsub#owner"); - Element configure = pubsub.addChild("configure").setAttribute("node",node); + Element pubsub = packet.addChild("pubsub", "http://jabber.org/protocol/pubsub#owner"); + Element configure = pubsub.addChild("configure").setAttribute("node", node); if (data != null) { configure.addChild(data); } diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 5ebe20bb6..394c25c10 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -106,7 +106,8 @@ public class MessageGenerator extends AbstractGenerator { final URL url = fileParams.url; if (P1S3UrlStreamHandler.PROTOCOL_NAME.equals(url.getProtocol())) { Element x = packet.addChild("x", Namespace.P1_S3_FILE_TRANSFER); - x.setAttribute("name", url.getFile()); + final String file = url.getFile(); + x.setAttribute("name", file.charAt(0) == '/' ? file.substring(1) : file); x.setAttribute("fileid", url.getHost()); return packet; } else { @@ -127,7 +128,8 @@ public class MessageGenerator extends AbstractGenerator { final URL url = fileParams.url; if (P1S3UrlStreamHandler.PROTOCOL_NAME.equals(url.getProtocol())) { Element x = packet.addChild("x", Namespace.P1_S3_FILE_TRANSFER); - x.setAttribute("name", url.getFile()); + final String file = url.getFile(); + x.setAttribute("name", file.charAt(0) == '/' ? file.substring(1) : file); x.setAttribute("fileid", url.getHost()); } else { packet.setBody(url.toString()); diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java index 60f180bde..aa8e9c464 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.http; import android.os.PowerManager; +import android.support.annotation.Nullable; import android.util.Log; import java.io.BufferedInputStream; @@ -17,6 +18,7 @@ import javax.net.ssl.SSLHandshakeException; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Transferable; @@ -27,6 +29,9 @@ 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 { @@ -39,8 +44,9 @@ public class HttpDownloadConnection implements Transferable { private int mStatus = Transferable.STATUS_UNKNOWN; private boolean acceptedAutomatically = false; private int mProgress = 0; - private boolean mUseTor = false; + private final boolean mUseTor; private boolean canceled = false; + private Method method = Method.HTTP_UPLOAD; public HttpDownloadConnection(HttpConnectionManager manager) { this.mHttpConnectionManager = manager; @@ -100,6 +106,7 @@ public class HttpDownloadConnection implements Transferable { 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; checkFileSize(interactive); } catch (MalformedURLException e) { this.cancel(); @@ -153,7 +160,7 @@ public class HttpDownloadConnection implements Transferable { } } - public void updateProgress(long i) { + private void updateProgress(long i) { this.mProgress = (int) i; mHttpConnectionManager.updateConversationUi(false); } @@ -179,27 +186,64 @@ public class HttpDownloadConnection implements Transferable { private class FileSizeChecker implements Runnable { - private boolean interactive = false; + private final boolean interactive; - public FileSizeChecker(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) { - changeStatus(STATUS_OFFER_CHECK_FILESIZE); Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage()); - if (interactive) { - showToastForException(e); - } else { - HttpDownloadConnection.this.acceptedAutomatically = false; - HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message); - } - cancel(); + retrieveFailed(e); return; } file.setExpectedSize(size); @@ -226,10 +270,14 @@ public class HttpDownloadConnection implements Transferable { } else { connection = (HttpURLConnection) mUrl.openConnection(); } - connection.setRequestMethod("HEAD"); + 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()); - Log.d(Config.LOGTAG, "connection: " + connection.toString()); connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getIdentityName()); if (connection instanceof HttpsURLConnection) { mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive); @@ -237,7 +285,18 @@ public class HttpDownloadConnection implements Transferable { connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000); connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000); connection.connect(); - String contentLength = connection.getHeaderField("Content-Length"); + 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"); @@ -255,7 +314,7 @@ public class HttpDownloadConnection implements Transferable { private class FileDownloader implements Runnable { - private boolean interactive = false; + private final boolean interactive; private OutputStream os; @@ -374,7 +433,9 @@ public class HttpDownloadConnection implements Transferable { message.setType(Message.TYPE_FILE); final URL url; final String ref = mUrl.getRef(); - if (ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches()) { + 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;