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 final long start = mam.getStart();
261 final long end = mam.getEnd();
262 if (start != 0) {
263 data.put("start", getTimestamp(start));
264 }
265 if (end != 0) {
266 data.put("end", getTimestamp(end));
267 }
268 data.submit();
269 query.addChild(data);
270 Element set = query.addChild("set", "http://jabber.org/protocol/rsm");
271 if (mam.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
272 set.addChild("before").setContent(mam.getReference());
273 } else if (mam.getReference() != null) {
274 set.addChild("after").setContent(mam.getReference());
275 }
276 set.addChild("max").setContent(String.valueOf(Config.PAGE_SIZE));
277 return packet;
278 }
279 public IqPacket generateGetBlockList() {
280 final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
281 iq.addChild("blocklist", Namespace.BLOCKING);
282
283 return iq;
284 }
285
286 public IqPacket generateSetBlockRequest(final Jid jid, boolean reportSpam) {
287 final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
288 final Element block = iq.addChild("block", Namespace.BLOCKING);
289 final Element item = block.addChild("item").setAttribute("jid", jid.asBareJid().toString());
290 if (reportSpam) {
291 item.addChild("report", "urn:xmpp:reporting:0").addChild("spam");
292 }
293 Log.d(Config.LOGTAG,iq.toString());
294 return iq;
295 }
296
297 public IqPacket generateSetUnblockRequest(final Jid jid) {
298 final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
299 final Element block = iq.addChild("unblock", Namespace.BLOCKING);
300 block.addChild("item").setAttribute("jid", jid.asBareJid().toString());
301 return iq;
302 }
303
304 public IqPacket generateSetPassword(final Account account, final String newPassword) {
305 final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
306 packet.setTo(Jid.of(account.getServer()));
307 final Element query = packet.addChild("query", Namespace.REGISTER);
308 final Jid jid = account.getJid();
309 query.addChild("username").setContent(jid.getLocal());
310 query.addChild("password").setContent(newPassword);
311 return packet;
312 }
313
314 public IqPacket changeAffiliation(Conversation conference, Jid jid, String affiliation) {
315 List<Jid> jids = new ArrayList<>();
316 jids.add(jid);
317 return changeAffiliation(conference,jids,affiliation);
318 }
319
320 public IqPacket changeAffiliation(Conversation conference, List<Jid> jids, String affiliation) {
321 IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
322 packet.setTo(conference.getJid().asBareJid());
323 packet.setFrom(conference.getAccount().getJid());
324 Element query = packet.query("http://jabber.org/protocol/muc#admin");
325 for(Jid jid : jids) {
326 Element item = query.addChild("item");
327 item.setAttribute("jid", jid.toString());
328 item.setAttribute("affiliation", affiliation);
329 }
330 return packet;
331 }
332
333 public IqPacket changeRole(Conversation conference, String nick, String role) {
334 IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
335 packet.setTo(conference.getJid().asBareJid());
336 packet.setFrom(conference.getAccount().getJid());
337 Element item = packet.query("http://jabber.org/protocol/muc#admin").addChild("item");
338 item.setAttribute("nick", nick);
339 item.setAttribute("role", role);
340 return packet;
341 }
342
343 public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file, String mime) {
344 IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
345 packet.setTo(host);
346 Element request = packet.addChild("request", Namespace.HTTP_UPLOAD);
347 request.setAttribute("filename",convertFilename(file.getName()));
348 request.setAttribute("size",file.getExpectedSize());
349 request.setAttribute("content-type",mime);
350 return packet;
351 }
352
353 private static String convertFilename(String name) {
354 int pos = name.indexOf('.');
355 if (pos != -1) {
356 try {
357 UUID uuid = UUID.fromString(name.substring(0, pos));
358 ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
359 bb.putLong(uuid.getMostSignificantBits());
360 bb.putLong(uuid.getLeastSignificantBits());
361 return Base64.encodeToString(bb.array(), Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP) + name.substring(pos, name.length());
362 } catch (Exception e) {
363 return name;
364 }
365 } else {
366 return name;
367 }
368 }
369
370 public IqPacket generateCreateAccountWithCaptcha(Account account, String id, Data data) {
371 final IqPacket register = new IqPacket(IqPacket.TYPE.SET);
372 register.setFrom(account.getJid().asBareJid());
373 register.setTo(Jid.of(account.getServer()));
374 register.setId(id);
375 Element query = register.query("jabber:iq:register");
376 if (data != null) {
377 query.addChild(data);
378 }
379 return register;
380 }
381
382 public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId) {
383 IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
384 packet.setTo(appServer);
385 Element command = packet.addChild("command", "http://jabber.org/protocol/commands");
386 command.setAttribute("node","register-push-gcm");
387 command.setAttribute("action","execute");
388 Data data = new Data();
389 data.put("token", token);
390 data.put("android-id", deviceId);
391 data.submit();
392 command.addChild(data);
393 return packet;
394 }
395
396 public IqPacket enablePush(Jid jid, String node, String secret) {
397 IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
398 Element enable = packet.addChild("enable","urn:xmpp:push:0");
399 enable.setAttribute("jid",jid.toString());
400 enable.setAttribute("node", node);
401 Data data = new Data();
402 data.setFormType(Namespace.PUBSUB_PUBLISH_OPTIONS);
403 data.put("secret",secret);
404 data.submit();
405 enable.addChild(data);
406 return packet;
407 }
408
409 public IqPacket queryAffiliation(Conversation conversation, String affiliation) {
410 IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
411 packet.setTo(conversation.getJid().asBareJid());
412 packet.query("http://jabber.org/protocol/muc#admin").addChild("item").setAttribute("affiliation",affiliation);
413 return packet;
414 }
415
416 public static Bundle defaultRoomConfiguration() {
417 Bundle options = new Bundle();
418 options.putString("muc#roomconfig_persistentroom", "1");
419 options.putString("muc#roomconfig_membersonly", "1");
420 options.putString("muc#roomconfig_publicroom", "0");
421 options.putString("muc#roomconfig_whois", "anyone");
422 options.putString("muc#roomconfig_enablearchiving","1");
423 options.putString("mam","1");
424 return options;
425 }
426
427 public IqPacket requestPubsubConfiguration(Jid jid, String node) {
428 return pubsubConfiguration(jid, node, null);
429 }
430
431 public IqPacket publishPubsubConfiguration(Jid jid, String node, Data data) {
432 return pubsubConfiguration(jid,node,data);
433 }
434
435 private IqPacket pubsubConfiguration(Jid jid, String node, Data data) {
436 IqPacket packet = new IqPacket(data == null ? IqPacket.TYPE.GET : IqPacket.TYPE.SET);
437 packet.setTo(jid);
438 Element pubsub = packet.addChild("pubsub","http://jabber.org/protocol/pubsub#owner");
439 Element configure = pubsub.addChild("configure").setAttribute("node",node);
440 if (data != null) {
441 configure.addChild(data);
442 }
443 return packet;
444 }
445}