/* * Kontalk Java client * Copyright (C) 2016 Kontalk Devteam <devteam@kontalk.org> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kontalk.client; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Optional; import java.util.logging.Logger; import java.util.stream.Collectors; import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smackx.address.packet.MultipleAddresses; import org.jivesoftware.smackx.chatstates.ChatState; import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension; import org.jivesoftware.smackx.receipts.DeliveryReceiptRequest; import org.jxmpp.jid.Jid; import org.kontalk.client.OpenPGPExtension.SignCryptElement; import org.kontalk.misc.JID; import org.kontalk.model.chat.Chat; import org.kontalk.model.chat.GroupChat.KonGroupChat; import org.kontalk.model.chat.GroupMetaData.KonGroupData; import org.kontalk.model.message.KonMessage; import org.kontalk.model.message.MessageContent; import org.kontalk.model.message.MessageContent.OutAttachment; import org.kontalk.model.message.MessageContent.Preview; import org.kontalk.model.message.OutMessage; import org.kontalk.model.message.Transmission; import org.kontalk.util.MessageUtils.SendTask; import org.kontalk.util.MessageUtils.SendTask.Encryption; import org.kontalk.util.ClientUtils; import org.kontalk.util.EncodingUtils; import org.kontalk.client.OpenPGPExtension.BodyElement; /** * * @author Alexander Bikadorov {@literal <bikaejkb@mail.tu-berlin.de>} */ public final class KonMessageSender { private static final Logger LOGGER = Logger.getLogger(KonMessageSender.class.getName()); private static final int RPAD_LENGTH_RANGE = 40; private final Client mClient; KonMessageSender(Client client) { mClient = client; } boolean sendMessage(SendTask task, Optional<Jid> multiAddressHost) { OutMessage message = task.message; // check for correct receipt status and reset it KonMessage.Status status = message.getStatus(); assert status == KonMessage.Status.PENDING || status == KonMessage.Status.ERROR; message.setStatus(KonMessage.Status.PENDING); if (!mClient.isConnected()) { LOGGER.info("not sending message(s), not connected"); return false; } MessageContent content = message.getContent(); OutAttachment att = content.getOutAttachment().orElse(null); if (att != null && !att.hasURL()) { LOGGER.warning("attachment not uploaded"); message.setStatus(KonMessage.Status.ERROR); return false; } boolean encrypted = task.encryption != Encryption.NONE; Chat chat = message.getChat(); Message smackMessage = encrypted ? new Message() : rawMessage(content, chat, false); smackMessage.setType(Message.Type.chat); smackMessage.setStanzaId(message.getXMPPID()); String threadID = chat.getXMPPID(); if (!threadID.isEmpty()) smackMessage.setThread(threadID); // extensions // not with group chat (at least not for Kontalk groups or MUC) if (!chat.isGroupChat()) smackMessage.addExtension(new DeliveryReceiptRequest()); if (smackMessage.getBody() == null) // TEMP: server bug workaround, always include body smackMessage.setBody(encrypted ? // Using implicit Enum.toString() "This message is encrypted using OpenPGP (" + task.encryption +")." : "dummy"); if (task.sendChatState) smackMessage.addExtension(new ChatStateExtension(ChatState.active)); if (encrypted) { String encryptedData = task.getEncryptedData(); if (encryptedData.isEmpty()) { LOGGER.warning("no encrypted data"); return false; } ExtensionElement encryptionExtension; switch(task.encryption) { case RFC3923: encryptionExtension = new E2EEncryption(encryptedData); break; case XEP0373: encryptionExtension = new OpenPGPExtension(encryptedData); break; default: LOGGER.warning("unknown encryption: " + task.encryption); return false; } smackMessage.addExtension(encryptionExtension); } List<JID> JIDs = message.getTransmissions().stream() .map(Transmission::getJID) .collect(Collectors.toList()); if (JIDs.size() > 1 && multiAddressHost.isPresent()) { // send one message to multiple receiver using XEP-0033 smackMessage.setTo(multiAddressHost.get()); MultipleAddresses addresses = new MultipleAddresses(); for (JID to: JIDs) { addresses.addAddress(MultipleAddresses.Type.to, to.toBareSmack(), null, null, false, null); } smackMessage.addExtension(addresses); return mClient.sendPacket(smackMessage); } else { // only one receiver or fallback: send one message to each receiver ArrayList<Message> sendMessages = new ArrayList<>(); for (JID to: JIDs) { Message sendMessage = smackMessage.clone(); sendMessage.setTo(to.toBareSmack()); sendMessages.add(sendMessage); } return mClient.sendPackets(sendMessages.toArray(new Message[0])); } } public static String getEncryptionPayloadRFC3923(MessageContent content, Chat chat) { return rawMessage(content, chat, true).toXML().toString(); } private static Message rawMessage(MessageContent content, Chat chat, boolean encrypted) { Message smackMessage = new Message(); // text body String text = content.getPlainText(); OutAttachment att = content.getOutAttachment().orElse(null); if (text.isEmpty() && att != null) { // use attachment URL as body text = att.getURL().toString(); } if (!text.isEmpty()) smackMessage.setBody(text); extensionsForContent(content, chat, encrypted) .forEach(smackMessage::addExtension); return smackMessage; } /** Get XEP-0373 signcrypt plaintext as XML string. */ public static String getSignCryptElement(OutMessage message) { List<String> tos = message.getTransmissions().stream() .map(t -> t.getJID().string()) .collect(Collectors.toList()); int rpadLength = new SecureRandom().nextInt(RPAD_LENGTH_RANGE); List<ExtensionElement> contentElements = new ArrayList<>(); MessageContent content = message.getContent(); String text = content.getPlainText(); if (!text.isEmpty()) contentElements.add(new BodyElement(text)); contentElements.addAll(extensionsForContent(content, message.getChat(), true)); return new SignCryptElement(tos, new Date(), rpadLength, contentElements) .toXML().toString(); } private static List<ExtensionElement> extensionsForContent(MessageContent content, Chat chat, boolean encrypted) { List<ExtensionElement> elements = new ArrayList<>(); // attachment OutAttachment att = content.getOutAttachment().orElse(null); if (att != null) { OutOfBandData oobData = new OutOfBandData(att.getURL().toString(), att.getMimeType(), att.getLength(), encrypted); elements.add(oobData); Preview preview = content.getPreview().orElse(null); if (preview != null) { String data = EncodingUtils.bytesToBase64(preview.getData()); BitsOfBinary bob = new BitsOfBinary(preview.getMimeType(), data); elements.add(bob); } } // group command if (chat instanceof KonGroupChat) { KonGroupChat groupChat = (KonGroupChat) chat; KonGroupData gid = groupChat.getGroupData(); MessageContent.GroupCommand groupCommand = content.getGroupCommand().orElse(null); elements.add(groupCommand != null ? ClientUtils.groupCommandToGroupExtension(groupChat, groupCommand) : new GroupExtension(gid.id, gid.owner.string())); } return elements; } }