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