IqGenerator.java

  1package eu.siacs.conversations.generator;
  2
  3import android.os.Bundle;
  4import android.util.Base64;
  5import android.util.Log;
  6import eu.siacs.conversations.Config;
  7import eu.siacs.conversations.crypto.axolotl.AxolotlService;
  8import eu.siacs.conversations.entities.Account;
  9import eu.siacs.conversations.entities.Bookmark;
 10import eu.siacs.conversations.entities.Conversation;
 11import eu.siacs.conversations.entities.DownloadableFile;
 12import eu.siacs.conversations.services.MessageArchiveService;
 13import eu.siacs.conversations.services.XmppConnectionService;
 14import eu.siacs.conversations.xml.Element;
 15import eu.siacs.conversations.xml.Namespace;
 16import eu.siacs.conversations.xmpp.Jid;
 17import eu.siacs.conversations.xmpp.forms.Data;
 18import eu.siacs.conversations.xmpp.pep.Avatar;
 19import im.conversations.android.xmpp.model.stanza.Iq;
 20import im.conversations.android.xmpp.model.upload.Request;
 21import java.nio.ByteBuffer;
 22import java.security.cert.CertificateEncodingException;
 23import java.security.cert.X509Certificate;
 24import java.util.ArrayList;
 25import java.util.List;
 26import java.util.Locale;
 27import java.util.Set;
 28import java.util.TimeZone;
 29import java.util.UUID;
 30import org.whispersystems.libsignal.IdentityKey;
 31import org.whispersystems.libsignal.ecc.ECPublicKey;
 32import org.whispersystems.libsignal.state.PreKeyRecord;
 33import org.whispersystems.libsignal.state.SignedPreKeyRecord;
 34
 35public class IqGenerator extends AbstractGenerator {
 36
 37    public IqGenerator(final XmppConnectionService service) {
 38        super(service);
 39    }
 40
 41    public Iq entityTimeResponse(final Iq request) {
 42        final Iq packet = request.generateResponse(Iq.Type.RESULT);
 43        Element time = packet.addChild("time", "urn:xmpp:time");
 44        final long now = System.currentTimeMillis();
 45        time.addChild("utc").setContent(getTimestamp(now));
 46        TimeZone ourTimezone = TimeZone.getDefault();
 47        long offsetSeconds = ourTimezone.getOffset(now) / 1000;
 48        long offsetMinutes = Math.abs((offsetSeconds % 3600) / 60);
 49        long offsetHours = offsetSeconds / 3600;
 50        String hours;
 51        if (offsetHours < 0) {
 52            hours = String.format(Locale.US, "%03d", offsetHours);
 53        } else {
 54            hours = String.format(Locale.US, "%02d", offsetHours);
 55        }
 56        String minutes = String.format(Locale.US, "%02d", offsetMinutes);
 57        time.addChild("tzo").setContent(hours + ":" + minutes);
 58        return packet;
 59    }
 60
 61    public static Iq purgeOfflineMessages() {
 62        final Iq packet = new Iq(Iq.Type.SET);
 63        packet.addChild("offline", Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL).addChild("purge");
 64        return packet;
 65    }
 66
 67    protected Iq publish(final String node, final Element item, final Bundle options) {
 68        final var packet = new Iq(Iq.Type.SET);
 69        final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB);
 70        final Element publish = pubsub.addChild("publish");
 71        publish.setAttribute("node", node);
 72        publish.addChild(item);
 73        if (options != null) {
 74            final Element publishOptions = pubsub.addChild("publish-options");
 75            publishOptions.addChild(Data.create(Namespace.PUBSUB_PUBLISH_OPTIONS, options));
 76        }
 77        return packet;
 78    }
 79
 80    protected Iq publish(final String node, final Element item) {
 81        return publish(node, item, null);
 82    }
 83
 84    private Iq retrieve(String node, Element item) {
 85        final var packet = new Iq(Iq.Type.GET);
 86        final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB);
 87        final Element items = pubsub.addChild("items");
 88        items.setAttribute("node", node);
 89        if (item != null) {
 90            items.addChild(item);
 91        }
 92        return packet;
 93    }
 94
 95    public Iq retrieveBookmarks() {
 96        return retrieve(Namespace.BOOKMARKS2, null);
 97    }
 98
 99    public Iq retrieveMds() {
100        return retrieve(Namespace.MDS_DISPLAYED, null);
101    }
102
103    public Iq publishNick(String nick) {
104        final Element item = new Element("item");
105        item.setAttribute("id", "current");
106        item.addChild("nick", Namespace.NICK).setContent(nick);
107        return publish(Namespace.NICK, item);
108    }
109
110    public Iq deleteNode(final String node) {
111        final var packet = new Iq(Iq.Type.SET);
112        final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB_OWNER);
113        pubsub.addChild("delete").setAttribute("node", node);
114        return packet;
115    }
116
117    public Iq deleteItem(final String node, final String id) {
118        final var packet = new Iq(Iq.Type.SET);
119        final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB);
120        final Element retract = pubsub.addChild("retract");
121        retract.setAttribute("node", node);
122        retract.setAttribute("notify", "true");
123        retract.addChild("item").setAttribute("id", id);
124        return packet;
125    }
126
127    public Iq publishAvatar(Avatar avatar, Bundle options) {
128        final Element item = new Element("item");
129        item.setAttribute("id", avatar.sha1sum);
130        final Element data = item.addChild("data", Namespace.AVATAR_DATA);
131        data.setContent(avatar.image);
132        return publish(Namespace.AVATAR_DATA, item, options);
133    }
134
135    public Iq publishElement(
136            final String namespace, final Element element, String id, final Bundle options) {
137        final Element item = new Element("item");
138        item.setAttribute("id", id);
139        item.addChild(element);
140        return publish(namespace, item, options);
141    }
142
143    public Iq publishAvatarMetadata(final Avatar avatar, final Bundle options) {
144        final Element item = new Element("item");
145        item.setAttribute("id", avatar.sha1sum);
146        final Element metadata = item.addChild("metadata", Namespace.AVATAR_METADATA);
147        final Element info = metadata.addChild("info");
148        info.setAttribute("bytes", avatar.size);
149        info.setAttribute("id", avatar.sha1sum);
150        info.setAttribute("height", avatar.height);
151        info.setAttribute("width", avatar.height);
152        info.setAttribute("type", avatar.type);
153        return publish(Namespace.AVATAR_METADATA, item, options);
154    }
155
156    public Iq retrievePepAvatar(final Avatar avatar) {
157        final Element item = new Element("item");
158        item.setAttribute("id", avatar.sha1sum);
159        final var packet = retrieve(Namespace.AVATAR_DATA, item);
160        packet.setTo(avatar.owner);
161        return packet;
162    }
163
164    public Iq retrieveVcardAvatar(final Avatar avatar) {
165        final Iq packet = new Iq(Iq.Type.GET);
166        packet.setTo(avatar.owner);
167        packet.addChild("vCard", "vcard-temp");
168        return packet;
169    }
170
171    public Iq retrieveVcardAvatar(final Jid to) {
172        final Iq packet = new Iq(Iq.Type.GET);
173        packet.setTo(to);
174        packet.addChild("vCard", "vcard-temp");
175        return packet;
176    }
177
178    public Iq retrieveAvatarMetaData(final Jid to) {
179        final Iq packet = retrieve("urn:xmpp:avatar:metadata", null);
180        if (to != null) {
181            packet.setTo(to);
182        }
183        return packet;
184    }
185
186    public Iq retrieveDeviceIds(final Jid to) {
187        final var packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null);
188        if (to != null) {
189            packet.setTo(to);
190        }
191        return packet;
192    }
193
194    public Iq retrieveBundlesForDevice(final Jid to, final int deviceid) {
195        final var packet = retrieve(AxolotlService.PEP_BUNDLES + ":" + deviceid, null);
196        packet.setTo(to);
197        return packet;
198    }
199
200    public Iq retrieveVerificationForDevice(final Jid to, final int deviceid) {
201        final var packet = retrieve(AxolotlService.PEP_VERIFICATION + ":" + deviceid, null);
202        packet.setTo(to);
203        return packet;
204    }
205
206    public Iq publishDeviceIds(final Set<Integer> ids, final Bundle publishOptions) {
207        final Element item = new Element("item");
208        item.setAttribute("id", "current");
209        final Element list = item.addChild("list", AxolotlService.PEP_PREFIX);
210        for (Integer id : ids) {
211            final Element device = new Element("device");
212            device.setAttribute("id", id);
213            list.addChild(device);
214        }
215        return publish(AxolotlService.PEP_DEVICE_LIST, item, publishOptions);
216    }
217
218    public Element publishBookmarkItem(final Bookmark bookmark) {
219        final String name = bookmark.getBookmarkName();
220        final String nick = bookmark.getNick();
221        final String password = bookmark.getPassword();
222        final boolean autojoin = bookmark.autojoin();
223        final Element conference = new Element("conference", Namespace.BOOKMARKS2);
224        if (name != null) {
225            conference.setAttribute("name", name);
226        }
227        if (nick != null) {
228            conference.addChild("nick").setContent(nick);
229        }
230        if (password != null) {
231            conference.addChild("password").setContent(password);
232        }
233        conference.setAttribute("autojoin", String.valueOf(autojoin));
234        conference.addChild(bookmark.getExtensions());
235        return conference;
236    }
237
238    public Element mdsDisplayed(final String stanzaId, final Conversation conversation) {
239        final Jid by;
240        if (conversation.getMode() == Conversation.MODE_MULTI) {
241            by = conversation.getJid().asBareJid();
242        } else {
243            by = conversation.getAccount().getJid().asBareJid();
244        }
245        return mdsDisplayed(stanzaId, by);
246    }
247
248    private Element mdsDisplayed(final String stanzaId, final Jid by) {
249        final Element displayed = new Element("displayed", Namespace.MDS_DISPLAYED);
250        final Element stanzaIdElement = displayed.addChild("stanza-id", Namespace.STANZA_IDS);
251        stanzaIdElement.setAttribute("id", stanzaId);
252        stanzaIdElement.setAttribute("by", by);
253        return displayed;
254    }
255
256    public Iq publishBundles(
257            final SignedPreKeyRecord signedPreKeyRecord,
258            final IdentityKey identityKey,
259            final Set<PreKeyRecord> preKeyRecords,
260            final int deviceId,
261            Bundle publishOptions) {
262        final Element item = new Element("item");
263        item.setAttribute("id", "current");
264        final Element bundle = item.addChild("bundle", AxolotlService.PEP_PREFIX);
265        final Element signedPreKeyPublic = bundle.addChild("signedPreKeyPublic");
266        signedPreKeyPublic.setAttribute("signedPreKeyId", signedPreKeyRecord.getId());
267        ECPublicKey publicKey = signedPreKeyRecord.getKeyPair().getPublicKey();
268        signedPreKeyPublic.setContent(Base64.encodeToString(publicKey.serialize(), Base64.NO_WRAP));
269        final Element signedPreKeySignature = bundle.addChild("signedPreKeySignature");
270        signedPreKeySignature.setContent(
271                Base64.encodeToString(signedPreKeyRecord.getSignature(), Base64.NO_WRAP));
272        final Element identityKeyElement = bundle.addChild("identityKey");
273        identityKeyElement.setContent(
274                Base64.encodeToString(identityKey.serialize(), Base64.NO_WRAP));
275
276        final Element prekeys = bundle.addChild("prekeys", AxolotlService.PEP_PREFIX);
277        for (PreKeyRecord preKeyRecord : preKeyRecords) {
278            final Element prekey = prekeys.addChild("preKeyPublic");
279            prekey.setAttribute("preKeyId", preKeyRecord.getId());
280            prekey.setContent(
281                    Base64.encodeToString(
282                            preKeyRecord.getKeyPair().getPublicKey().serialize(), Base64.NO_WRAP));
283        }
284
285        return publish(AxolotlService.PEP_BUNDLES + ":" + deviceId, item, publishOptions);
286    }
287
288    public Iq publishVerification(
289            byte[] signature, X509Certificate[] certificates, final int deviceId) {
290        final Element item = new Element("item");
291        item.setAttribute("id", "current");
292        final Element verification = item.addChild("verification", AxolotlService.PEP_PREFIX);
293        final Element chain = verification.addChild("chain");
294        for (int i = 0; i < certificates.length; ++i) {
295            try {
296                Element certificate = chain.addChild("certificate");
297                certificate.setContent(
298                        Base64.encodeToString(certificates[i].getEncoded(), Base64.NO_WRAP));
299                certificate.setAttribute("index", i);
300            } catch (CertificateEncodingException e) {
301                Log.d(Config.LOGTAG, "could not encode certificate");
302            }
303        }
304        verification
305                .addChild("signature")
306                .setContent(Base64.encodeToString(signature, Base64.NO_WRAP));
307        return publish(AxolotlService.PEP_VERIFICATION + ":" + deviceId, item);
308    }
309
310    public Iq queryMessageArchiveManagement(final MessageArchiveService.Query mam) {
311        final Iq packet = new Iq(Iq.Type.SET);
312        final Element query = packet.query(mam.version.namespace);
313        query.setAttribute("queryid", mam.getQueryId());
314        final Data data = new Data();
315        data.setFormType(mam.version.namespace);
316        if (mam.muc()) {
317            packet.setTo(mam.getWith());
318        } else if (mam.getWith() != null) {
319            data.put("with", mam.getWith().toString());
320        }
321        final long start = mam.getStart();
322        final long end = mam.getEnd();
323        if (start != 0) {
324            data.put("start", getTimestamp(start));
325        }
326        if (end != 0) {
327            data.put("end", getTimestamp(end));
328        }
329        data.submit();
330        query.addChild(data);
331        Element set = query.addChild("set", "http://jabber.org/protocol/rsm");
332        if (mam.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
333            set.addChild("before").setContent(mam.getReference());
334        } else if (mam.getReference() != null) {
335            set.addChild("after").setContent(mam.getReference());
336        }
337        set.addChild("max").setContent(String.valueOf(Config.PAGE_SIZE));
338        return packet;
339    }
340
341    public Iq generateGetBlockList() {
342        final Iq iq = new Iq(Iq.Type.GET);
343        iq.addChild("blocklist", Namespace.BLOCKING);
344
345        return iq;
346    }
347
348    public Iq generateSetBlockRequest(
349            final Jid jid, final boolean reportSpam, final String serverMsgId) {
350        final Iq iq = new Iq(Iq.Type.SET);
351        final Element block = iq.addChild("block", Namespace.BLOCKING);
352        final Element item = block.addChild("item").setAttribute("jid", jid);
353        if (reportSpam) {
354            final Element report = item.addChild("report", Namespace.REPORTING);
355            report.setAttribute("reason", Namespace.REPORTING_REASON_SPAM);
356            if (serverMsgId != null) {
357                final Element stanzaId = report.addChild("stanza-id", Namespace.STANZA_IDS);
358                stanzaId.setAttribute("by", jid);
359                stanzaId.setAttribute("id", serverMsgId);
360            }
361        }
362        Log.d(Config.LOGTAG, iq.toString());
363        return iq;
364    }
365
366    public Iq generateSetUnblockRequest(final Jid jid) {
367        final Iq iq = new Iq(Iq.Type.SET);
368        final Element block = iq.addChild("unblock", Namespace.BLOCKING);
369        block.addChild("item").setAttribute("jid", jid);
370        return iq;
371    }
372
373    public Iq generateSetPassword(final Account account, final String newPassword) {
374        final Iq packet = new Iq(Iq.Type.SET);
375        packet.setTo(account.getDomain());
376        final Element query = packet.addChild("query", Namespace.REGISTER);
377        final Jid jid = account.getJid();
378        query.addChild("username").setContent(jid.getLocal());
379        query.addChild("password").setContent(newPassword);
380        return packet;
381    }
382
383    public Iq changeAffiliation(Conversation conference, Jid jid, String affiliation) {
384        List<Jid> jids = new ArrayList<>();
385        jids.add(jid);
386        return changeAffiliation(conference, jids, affiliation);
387    }
388
389    public Iq changeAffiliation(Conversation conference, List<Jid> jids, String affiliation) {
390        final Iq packet = new Iq(Iq.Type.SET);
391        packet.setTo(conference.getJid().asBareJid());
392        packet.setFrom(conference.getAccount().getJid());
393        Element query = packet.query("http://jabber.org/protocol/muc#admin");
394        for (Jid jid : jids) {
395            Element item = query.addChild("item");
396            item.setAttribute("jid", jid);
397            item.setAttribute("affiliation", affiliation);
398        }
399        return packet;
400    }
401
402    public Iq changeRole(Conversation conference, String nick, String role) {
403        final Iq packet = new Iq(Iq.Type.SET);
404        packet.setTo(conference.getJid().asBareJid());
405        packet.setFrom(conference.getAccount().getJid());
406        Element item = packet.query("http://jabber.org/protocol/muc#admin").addChild("item");
407        item.setAttribute("nick", nick);
408        item.setAttribute("role", role);
409        return packet;
410    }
411
412    public Iq requestHttpUploadSlot(
413            final Jid host, final DownloadableFile file, final String mime) {
414        final Iq packet = new Iq(Iq.Type.GET);
415        packet.setTo(host);
416        final var request = packet.addExtension(new Request());
417        request.setFilename(convertFilename(file.getName()));
418        request.setSize(file.getExpectedSize());
419        return packet;
420    }
421
422    private static String convertFilename(String name) {
423        int pos = name.indexOf('.');
424        if (pos != -1) {
425            try {
426                UUID uuid = UUID.fromString(name.substring(0, pos));
427                ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
428                bb.putLong(uuid.getMostSignificantBits());
429                bb.putLong(uuid.getLeastSignificantBits());
430                return Base64.encodeToString(
431                                bb.array(), Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP)
432                        + name.substring(pos);
433            } catch (Exception e) {
434                return name;
435            }
436        } else {
437            return name;
438        }
439    }
440
441    public static Iq generateCreateAccountWithCaptcha(
442            final Account account, final String id, final Data data) {
443        final Iq register = new Iq(Iq.Type.SET);
444        register.setFrom(account.getJid().asBareJid());
445        register.setTo(account.getDomain());
446        register.setId(id);
447        Element query = register.query(Namespace.REGISTER);
448        if (data != null) {
449            query.addChild(data);
450        }
451        return register;
452    }
453
454    public Iq pushTokenToAppServer(Jid appServer, String token, String deviceId) {
455        return pushTokenToAppServer(appServer, token, deviceId, null);
456    }
457
458    public Iq pushTokenToAppServer(Jid appServer, String token, String deviceId, Jid muc) {
459        final Iq packet = new Iq(Iq.Type.SET);
460        packet.setTo(appServer);
461        final Element command = packet.addChild("command", Namespace.COMMANDS);
462        command.setAttribute("node", "register-push-fcm");
463        command.setAttribute("action", "execute");
464        final Data data = new Data();
465        data.put("token", token);
466        data.put("android-id", deviceId);
467        if (muc != null) {
468            data.put("muc", muc.toString());
469        }
470        data.submit();
471        command.addChild(data);
472        return packet;
473    }
474
475    public Iq unregisterChannelOnAppServer(Jid appServer, String deviceId, String channel) {
476        final Iq packet = new Iq(Iq.Type.SET);
477        packet.setTo(appServer);
478        final Element command = packet.addChild("command", Namespace.COMMANDS);
479        command.setAttribute("node", "unregister-push-fcm");
480        command.setAttribute("action", "execute");
481        final Data data = new Data();
482        data.put("channel", channel);
483        data.put("android-id", deviceId);
484        data.submit();
485        command.addChild(data);
486        return packet;
487    }
488
489    public Iq enablePush(final Jid jid, final String node, final String secret) {
490        final Iq packet = new Iq(Iq.Type.SET);
491        Element enable = packet.addChild("enable", Namespace.PUSH);
492        enable.setAttribute("jid", jid);
493        enable.setAttribute("node", node);
494        if (secret != null) {
495            Data data = new Data();
496            data.setFormType(Namespace.PUBSUB_PUBLISH_OPTIONS);
497            data.put("secret", secret);
498            data.submit();
499            enable.addChild(data);
500        }
501        return packet;
502    }
503
504    public Iq disablePush(final Jid jid, final String node) {
505        Iq packet = new Iq(Iq.Type.SET);
506        Element disable = packet.addChild("disable", Namespace.PUSH);
507        disable.setAttribute("jid", jid);
508        disable.setAttribute("node", node);
509        return packet;
510    }
511
512    public Iq queryAffiliation(Conversation conversation, String affiliation) {
513        final Iq packet = new Iq(Iq.Type.GET);
514        packet.setTo(conversation.getJid().asBareJid());
515        packet.query("http://jabber.org/protocol/muc#admin")
516                .addChild("item")
517                .setAttribute("affiliation", affiliation);
518        return packet;
519    }
520
521    public static Bundle defaultGroupChatConfiguration() {
522        Bundle options = new Bundle();
523        options.putString("muc#roomconfig_persistentroom", "1");
524        options.putString("muc#roomconfig_membersonly", "1");
525        options.putString("muc#roomconfig_publicroom", "0");
526        options.putString("muc#roomconfig_whois", "anyone");
527        options.putString("muc#roomconfig_changesubject", "0");
528        options.putString("muc#roomconfig_allowinvites", "0");
529        options.putString("muc#roomconfig_enablearchiving", "1"); // prosody
530        options.putString("mam", "1"); // ejabberd community
531        options.putString("muc#roomconfig_mam", "1"); // ejabberd saas
532        return options;
533    }
534
535    public static Bundle defaultChannelConfiguration() {
536        Bundle options = new Bundle();
537        options.putString("muc#roomconfig_persistentroom", "1");
538        options.putString("muc#roomconfig_membersonly", "0");
539        options.putString("muc#roomconfig_publicroom", "1");
540        options.putString("muc#roomconfig_whois", "moderators");
541        options.putString("muc#roomconfig_changesubject", "0");
542        options.putString("muc#roomconfig_enablearchiving", "1"); // prosody
543        options.putString("mam", "1"); // ejabberd community
544        options.putString("muc#roomconfig_mam", "1"); // ejabberd saas
545        return options;
546    }
547
548    public Iq requestPubsubConfiguration(Jid jid, String node) {
549        return pubsubConfiguration(jid, node, null);
550    }
551
552    public Iq publishPubsubConfiguration(Jid jid, String node, Data data) {
553        return pubsubConfiguration(jid, node, data);
554    }
555
556    private Iq pubsubConfiguration(Jid jid, String node, Data data) {
557        final Iq packet = new Iq(data == null ? Iq.Type.GET : Iq.Type.SET);
558        packet.setTo(jid);
559        Element pubsub = packet.addChild("pubsub", "http://jabber.org/protocol/pubsub#owner");
560        Element configure = pubsub.addChild("configure").setAttribute("node", node);
561        if (data != null) {
562            configure.addChild(data);
563        }
564        return packet;
565    }
566}