1package eu.siacs.conversations.generator;
2
3
4import android.os.Bundle;
5import android.util.Base64;
6import android.util.Log;
7
8import org.whispersystems.libsignal.IdentityKey;
9import org.whispersystems.libsignal.ecc.ECPublicKey;
10import org.whispersystems.libsignal.state.PreKeyRecord;
11import org.whispersystems.libsignal.state.SignedPreKeyRecord;
12
13import java.nio.ByteBuffer;
14import java.security.cert.CertificateEncodingException;
15import java.security.cert.X509Certificate;
16import java.util.ArrayList;
17import java.util.List;
18import java.util.Locale;
19import java.util.Set;
20import java.util.TimeZone;
21import java.util.UUID;
22
23import eu.siacs.conversations.Config;
24import eu.siacs.conversations.R;
25import eu.siacs.conversations.crypto.axolotl.AxolotlService;
26import eu.siacs.conversations.entities.Account;
27import eu.siacs.conversations.entities.Conversation;
28import eu.siacs.conversations.entities.DownloadableFile;
29import eu.siacs.conversations.services.MessageArchiveService;
30import eu.siacs.conversations.services.XmppConnectionService;
31import eu.siacs.conversations.xml.Namespace;
32import eu.siacs.conversations.xml.Element;
33import eu.siacs.conversations.xmpp.forms.Data;
34import eu.siacs.conversations.xmpp.pep.Avatar;
35import eu.siacs.conversations.xmpp.stanzas.IqPacket;
36import rocks.xmpp.addr.Jid;
37
38public class IqGenerator extends AbstractGenerator {
39
40 public IqGenerator(final XmppConnectionService service) {
41 super(service);
42 }
43
44 public IqPacket discoResponse(final IqPacket request) {
45 final IqPacket packet = new IqPacket(IqPacket.TYPE.RESULT);
46 packet.setId(request.getId());
47 packet.setTo(request.getFrom());
48 final Element query = packet.addChild("query", "http://jabber.org/protocol/disco#info");
49 query.setAttribute("node", request.query().getAttribute("node"));
50 final Element identity = query.addChild("identity");
51 identity.setAttribute("category", "client");
52 identity.setAttribute("type", getIdentityType());
53 identity.setAttribute("name", getIdentityName());
54 for (final String feature : getFeatures()) {
55 query.addChild("feature").setAttribute("var", feature);
56 }
57 return packet;
58 }
59
60 public IqPacket versionResponse(final IqPacket request) {
61 final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT);
62 Element query = packet.query("jabber:iq:version");
63 query.addChild("name").setContent(mXmppConnectionService.getString(R.string.app_name));
64 query.addChild("version").setContent(getIdentityVersion());
65 if ("chromium".equals(android.os.Build.BRAND)) {
66 query.addChild("os").setContent("Chrome OS");
67 } else{
68 query.addChild("os").setContent("Android");
69 }
70 return packet;
71 }
72
73 public IqPacket entityTimeResponse(IqPacket request) {
74 final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT);
75 Element time = packet.addChild("time","urn:xmpp:time");
76 final long now = System.currentTimeMillis();
77 time.addChild("utc").setContent(getTimestamp(now));
78 TimeZone ourTimezone = TimeZone.getDefault();
79 long offsetSeconds = ourTimezone.getOffset(now) / 1000;
80 long offsetMinutes = Math.abs((offsetSeconds % 3600) / 60);
81 long offsetHours = offsetSeconds / 3600;
82 String hours;
83 if (offsetHours<0) {
84 hours = String.format(Locale.US,"%03d",offsetHours);
85 } else {
86 hours = String.format(Locale.US,"%02d",offsetHours);
87 }
88 String minutes = String.format(Locale.US,"%02d",offsetMinutes);
89 time.addChild("tzo").setContent(hours+":"+minutes);
90 return packet;
91 }
92
93 public IqPacket purgeOfflineMessages() {
94 final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
95 packet.addChild("offline",Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL).addChild("purge");
96 return packet;
97 }
98
99 protected IqPacket publish(final String node, final Element item, final Bundle options) {
100 final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
101 final Element pubsub = packet.addChild("pubsub",Namespace.PUBSUB);
102 final Element publish = pubsub.addChild("publish");
103 publish.setAttribute("node", node);
104 publish.addChild(item);
105 if (options != null) {
106 final Element publishOptions = pubsub.addChild("publish-options");
107 publishOptions.addChild(Data.create(Namespace.PUBSUB_PUBLISH_OPTIONS, options));
108 }
109 return packet;
110 }
111
112 protected IqPacket publish(final String node, final Element item) {
113 return publish(node,item,null);
114 }
115
116 protected IqPacket retrieve(String node, Element item) {
117 final IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
118 final Element pubsub = packet.addChild("pubsub",Namespace.PUBSUB);
119 final Element items = pubsub.addChild("items");
120 items.setAttribute("node", node);
121 if (item != null) {
122 items.addChild(item);
123 }
124 return packet;
125 }
126
127 public IqPacket publishNick(String nick) {
128 final Element item = new Element("item");
129 item.addChild("nick","http://jabber.org/protocol/nick").setContent(nick);
130 return publish("http://jabber.org/protocol/nick", item);
131 }
132
133 public IqPacket publishAvatar(Avatar avatar) {
134 final Element item = new Element("item");
135 item.setAttribute("id", avatar.sha1sum);
136 final Element data = item.addChild("data", "urn:xmpp:avatar:data");
137 data.setContent(avatar.image);
138 return publish("urn:xmpp:avatar:data", item);
139 }
140
141 public IqPacket publishAvatarMetadata(final Avatar avatar) {
142 final Element item = new Element("item");
143 item.setAttribute("id", avatar.sha1sum);
144 final Element metadata = item
145 .addChild("metadata", "urn:xmpp:avatar:metadata");
146 final Element info = metadata.addChild("info");
147 info.setAttribute("bytes", avatar.size);
148 info.setAttribute("id", avatar.sha1sum);
149 info.setAttribute("height", avatar.height);
150 info.setAttribute("width", avatar.height);
151 info.setAttribute("type", avatar.type);
152 return publish("urn:xmpp:avatar:metadata", item);
153 }
154
155 public IqPacket retrievePepAvatar(final Avatar avatar) {
156 final Element item = new Element("item");
157 item.setAttribute("id", avatar.sha1sum);
158 final IqPacket packet = retrieve("urn:xmpp:avatar:data", item);
159 packet.setTo(avatar.owner);
160 return packet;
161 }
162
163 public IqPacket retrieveVcardAvatar(final Avatar avatar) {
164 final IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
165 packet.setTo(avatar.owner);
166 packet.addChild("vCard", "vcard-temp");
167 return packet;
168 }
169
170 public IqPacket retrieveAvatarMetaData(final Jid to) {
171 final IqPacket packet = retrieve("urn:xmpp:avatar:metadata", null);
172 if (to != null) {
173 packet.setTo(to);
174 }
175 return packet;
176 }
177
178 public IqPacket retrieveDeviceIds(final Jid to) {
179 final IqPacket packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null);
180 if(to != null) {
181 packet.setTo(to);
182 }
183 return packet;
184 }
185
186 public IqPacket retrieveBundlesForDevice(final Jid to, final int deviceid) {
187 final IqPacket packet = retrieve(AxolotlService.PEP_BUNDLES+":"+deviceid, null);
188 packet.setTo(to);
189 return packet;
190 }
191
192 public IqPacket retrieveVerificationForDevice(final Jid to, final int deviceid) {
193 final IqPacket packet = retrieve(AxolotlService.PEP_VERIFICATION+":"+deviceid, null);
194 packet.setTo(to);
195 return packet;
196 }
197
198 public IqPacket publishDeviceIds(final Set<Integer> ids, final Bundle publishOptions) {
199 final Element item = new Element("item");
200 final Element list = item.addChild("list", AxolotlService.PEP_PREFIX);
201 for(Integer id:ids) {
202 final Element device = new Element("device");
203 device.setAttribute("id", id);
204 list.addChild(device);
205 }
206 return publish(AxolotlService.PEP_DEVICE_LIST, item, publishOptions);
207 }
208
209 public IqPacket publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey,
210 final Set<PreKeyRecord> preKeyRecords, final int deviceId, Bundle publishOptions) {
211 final Element item = new Element("item");
212 final Element bundle = item.addChild("bundle", AxolotlService.PEP_PREFIX);
213 final Element signedPreKeyPublic = bundle.addChild("signedPreKeyPublic");
214 signedPreKeyPublic.setAttribute("signedPreKeyId", signedPreKeyRecord.getId());
215 ECPublicKey publicKey = signedPreKeyRecord.getKeyPair().getPublicKey();
216 signedPreKeyPublic.setContent(Base64.encodeToString(publicKey.serialize(),Base64.DEFAULT));
217 final Element signedPreKeySignature = bundle.addChild("signedPreKeySignature");
218 signedPreKeySignature.setContent(Base64.encodeToString(signedPreKeyRecord.getSignature(),Base64.DEFAULT));
219 final Element identityKeyElement = bundle.addChild("identityKey");
220 identityKeyElement.setContent(Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT));
221
222 final Element prekeys = bundle.addChild("prekeys", AxolotlService.PEP_PREFIX);
223 for(PreKeyRecord preKeyRecord:preKeyRecords) {
224 final Element prekey = prekeys.addChild("preKeyPublic");
225 prekey.setAttribute("preKeyId", preKeyRecord.getId());
226 prekey.setContent(Base64.encodeToString(preKeyRecord.getKeyPair().getPublicKey().serialize(), Base64.DEFAULT));
227 }
228
229 return publish(AxolotlService.PEP_BUNDLES+":"+deviceId, item, publishOptions);
230 }
231
232 public IqPacket publishVerification(byte[] signature, X509Certificate[] certificates, final int deviceId) {
233 final Element item = new Element("item");
234 final Element verification = item.addChild("verification", AxolotlService.PEP_PREFIX);
235 final Element chain = verification.addChild("chain");
236 for(int i = 0; i < certificates.length; ++i) {
237 try {
238 Element certificate = chain.addChild("certificate");
239 certificate.setContent(Base64.encodeToString(certificates[i].getEncoded(), Base64.DEFAULT));
240 certificate.setAttribute("index",i);
241 } catch (CertificateEncodingException e) {
242 Log.d(Config.LOGTAG, "could not encode certificate");
243 }
244 }
245 verification.addChild("signature").setContent(Base64.encodeToString(signature, Base64.DEFAULT));
246 return publish(AxolotlService.PEP_VERIFICATION+":"+deviceId, item);
247 }
248
249 public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) {
250 final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
251 final Element query = packet.query(mam.isLegacy() ? Namespace.MAM_LEGACY : Namespace.MAM);
252 query.setAttribute("queryid", mam.getQueryId());
253 final Data data = new Data();
254 data.setFormType(mam.isLegacy() ? Namespace.MAM_LEGACY : Namespace.MAM);
255 if (mam.muc()) {
256 packet.setTo(mam.getWith());
257 } else if (mam.getWith()!=null) {
258 data.put("with", mam.getWith().toString());
259 }
260 if (mam.getStart() != 0) {
261 data.put("start", getTimestamp(mam.getStart()));
262 }
263 data.put("end", getTimestamp(mam.getEnd()));
264 data.submit();
265 query.addChild(data);
266 Element set = query.addChild("set", "http://jabber.org/protocol/rsm");
267 if (mam.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
268 set.addChild("before").setContent(mam.getReference());
269 } else if (mam.getReference() != null) {
270 set.addChild("after").setContent(mam.getReference());
271 }
272 set.addChild("max").setContent(String.valueOf(Config.PAGE_SIZE));
273 return packet;
274 }
275 public IqPacket generateGetBlockList() {
276 final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
277 iq.addChild("blocklist", Namespace.BLOCKING);
278
279 return iq;
280 }
281
282 public IqPacket generateSetBlockRequest(final Jid jid, boolean reportSpam) {
283 final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
284 final Element block = iq.addChild("block", Namespace.BLOCKING);
285 final Element item = block.addChild("item").setAttribute("jid", jid.asBareJid().toString());
286 if (reportSpam) {
287 item.addChild("report", "urn:xmpp:reporting:0").addChild("spam");
288 }
289 Log.d(Config.LOGTAG,iq.toString());
290 return iq;
291 }
292
293 public IqPacket generateSetUnblockRequest(final Jid jid) {
294 final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
295 final Element block = iq.addChild("unblock", Namespace.BLOCKING);
296 block.addChild("item").setAttribute("jid", jid.asBareJid().toString());
297 return iq;
298 }
299
300 public IqPacket generateSetPassword(final Account account, final String newPassword) {
301 final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
302 packet.setTo(Jid.of(account.getServer()));
303 final Element query = packet.addChild("query", Namespace.REGISTER);
304 final Jid jid = account.getJid();
305 query.addChild("username").setContent(jid.getLocal());
306 query.addChild("password").setContent(newPassword);
307 return packet;
308 }
309
310 public IqPacket changeAffiliation(Conversation conference, Jid jid, String affiliation) {
311 List<Jid> jids = new ArrayList<>();
312 jids.add(jid);
313 return changeAffiliation(conference,jids,affiliation);
314 }
315
316 public IqPacket changeAffiliation(Conversation conference, List<Jid> jids, String affiliation) {
317 IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
318 packet.setTo(conference.getJid().asBareJid());
319 packet.setFrom(conference.getAccount().getJid());
320 Element query = packet.query("http://jabber.org/protocol/muc#admin");
321 for(Jid jid : jids) {
322 Element item = query.addChild("item");
323 item.setAttribute("jid", jid.toString());
324 item.setAttribute("affiliation", affiliation);
325 }
326 return packet;
327 }
328
329 public IqPacket changeRole(Conversation conference, String nick, String role) {
330 IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
331 packet.setTo(conference.getJid().asBareJid());
332 packet.setFrom(conference.getAccount().getJid());
333 Element item = packet.query("http://jabber.org/protocol/muc#admin").addChild("item");
334 item.setAttribute("nick", nick);
335 item.setAttribute("role", role);
336 return packet;
337 }
338
339 public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file, String mime) {
340 IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
341 packet.setTo(host);
342 Element request = packet.addChild("request", Namespace.HTTP_UPLOAD);
343 request.setAttribute("filename",convertFilename(file.getName()));
344 request.setAttribute("size",file.getExpectedSize());
345 request.setAttribute("content-type",mime);
346 return packet;
347 }
348
349 private static String convertFilename(String name) {
350 int pos = name.indexOf('.');
351 if (pos != -1) {
352 try {
353 UUID uuid = UUID.fromString(name.substring(0, pos));
354 ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
355 bb.putLong(uuid.getMostSignificantBits());
356 bb.putLong(uuid.getLeastSignificantBits());
357 return Base64.encodeToString(bb.array(), Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP) + name.substring(pos, name.length());
358 } catch (Exception e) {
359 return name;
360 }
361 } else {
362 return name;
363 }
364 }
365
366 public IqPacket generateCreateAccountWithCaptcha(Account account, String id, Data data) {
367 final IqPacket register = new IqPacket(IqPacket.TYPE.SET);
368 register.setFrom(account.getJid().asBareJid());
369 register.setTo(Jid.of(account.getServer()));
370 register.setId(id);
371 Element query = register.query("jabber:iq:register");
372 if (data != null) {
373 query.addChild(data);
374 }
375 return register;
376 }
377
378 public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId) {
379 IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
380 packet.setTo(appServer);
381 Element command = packet.addChild("command", "http://jabber.org/protocol/commands");
382 command.setAttribute("node","register-push-gcm");
383 command.setAttribute("action","execute");
384 Data data = new Data();
385 data.put("token", token);
386 data.put("device-id", deviceId);
387 data.submit();
388 command.addChild(data);
389 return packet;
390 }
391
392 public IqPacket enablePush(Jid jid, String node, String secret) {
393 IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
394 Element enable = packet.addChild("enable","urn:xmpp:push:0");
395 enable.setAttribute("jid",jid.toString());
396 enable.setAttribute("node", node);
397 Data data = new Data();
398 data.setFormType(Namespace.PUBSUB_PUBLISH_OPTIONS);
399 data.put("secret",secret);
400 data.submit();
401 enable.addChild(data);
402 return packet;
403 }
404
405 public IqPacket queryAffiliation(Conversation conversation, String affiliation) {
406 IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
407 packet.setTo(conversation.getJid().asBareJid());
408 packet.query("http://jabber.org/protocol/muc#admin").addChild("item").setAttribute("affiliation",affiliation);
409 return packet;
410 }
411
412 public static Bundle defaultRoomConfiguration() {
413 Bundle options = new Bundle();
414 options.putString("muc#roomconfig_persistentroom", "1");
415 options.putString("muc#roomconfig_membersonly", "1");
416 options.putString("muc#roomconfig_publicroom", "0");
417 options.putString("muc#roomconfig_whois", "anyone");
418 options.putString("muc#roomconfig_enablearchiving","1");
419 options.putString("mam","1");
420 return options;
421 }
422
423 public IqPacket requestPubsubConfiguration(Jid jid, String node) {
424 return pubsubConfiguration(jid, node, null);
425 }
426
427 public IqPacket publishPubsubConfiguration(Jid jid, String node, Data data) {
428 return pubsubConfiguration(jid,node,data);
429 }
430
431 private IqPacket pubsubConfiguration(Jid jid, String node, Data data) {
432 IqPacket packet = new IqPacket(data == null ? IqPacket.TYPE.GET : IqPacket.TYPE.SET);
433 packet.setTo(jid);
434 Element pubsub = packet.addChild("pubsub","http://jabber.org/protocol/pubsub#owner");
435 Element configure = pubsub.addChild("configure").setAttribute("node",node);
436 if (data != null) {
437 configure.addChild(data);
438 }
439 return packet;
440 }
441}