/* * 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.system; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.logging.Logger; import java.util.stream.Collectors; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.XMPPError; import org.kontalk.client.Client; import org.kontalk.client.HKPClient; import org.kontalk.crypto.Coder; import org.kontalk.crypto.PGPUtils; import org.kontalk.misc.JID; import org.kontalk.misc.ViewEvent; import org.kontalk.model.Contact; import org.kontalk.model.Contact.Subscription; import org.kontalk.model.Model; import org.kontalk.persistence.Config; import org.kontalk.util.ClientUtils; /** * Process incoming roster and presence changes. * * @author Alexander Bikadorov {@literal <bikaejkb@mail.tu-berlin.de>} */ public final class RosterHandler { private static final Logger LOGGER = Logger.getLogger(RosterHandler.class.getName()); private final Control mControl; private final Client mClient; private final Model mModel; private static final List<String> KEY_SERVERS = Collections.singletonList( "pgp.mit.edu" // TODO: add CA for this //"pool.sks-keyservers.net" ); public enum Error { SERVER_NOT_FOUND } RosterHandler(Control control, Client client, Model model) { mControl = control; mClient = client; mModel = model; } public void onLoaded(List<ClientUtils.KonRosterEntry> entries) { for (ClientUtils.KonRosterEntry entry: entries) this.onEntryAdded(entry); // check for deleted entries List<JID> rosterJIDs = entries.stream().map(e -> e.jid).collect(Collectors.toList()); for (Contact contact : mModel.contacts().getAll(false, true)) if (!rosterJIDs.contains(contact.getJID())) this.onEntryDeleted(contact.getJID()); } public void onEntryAdded(ClientUtils.KonRosterEntry entry) { if (mModel.contacts().contains(entry.jid)) { this.onEntryUpdate(entry); return; } LOGGER.info("adding contact from roster, jid: "+entry.jid); String name = entry.name.equals(entry.jid.local()) && entry.jid.isHash() ? // this must be the hash string, don't use it as name "" : entry.name; Contact newContact = mControl.createContact(entry.jid, name).orElse(null); if (newContact == null) return; newContact.setSubscriptionStatus(entry.subscription); mControl.maySendKeyRequest(newContact); if (entry.subscription == Contact.Subscription.UNSUBSCRIBED) mControl.sendPresenceSubscription(entry.jid, Client.PresenceCommand.REQUEST); } public void onEntryDeleted(JID jid) { // NOTE: also called on rename Contact contact = mModel.contacts().get(jid).orElse(null); if (contact == null) { LOGGER.info("can't find contact with jid: "+jid); return; } // TODO detect if contact account still exists mControl.getViewControl().changed(new ViewEvent.ContactDeleted(contact)); } // NOTE: also called for every contact in roster on every (re-)connect public void onEntryUpdate(ClientUtils.KonRosterEntry entry) { Contact contact = mModel.contacts().get(entry.jid).orElse(null); if (contact == null) { LOGGER.info("can't find contact with jid: "+entry.jid); return; } // subscription may have changed contact.setSubscriptionStatus(entry.subscription); // maybe subscribed now mControl.maySendKeyRequest(contact); // name may have changed if (contact.getName().isEmpty() && !entry.name.equals(entry.jid.local())) contact.setName(entry.name); if (contact.getSubScription() == Subscription.SUBSCRIBED && (contact.getOnline() == Contact.Online.UNKNOWN || contact.getOnline() == Contact.Online.NO)) mClient.sendLastActivityRequest(contact.getJID()); } public void onSubscriptionRequest(JID jid, byte[] rawKey) { Contact contact = mModel.contacts().get(jid).orElse(null); if (contact == null) return; if (Config.getInstance().getBoolean(Config.NET_AUTO_SUBSCRIPTION)) { mControl.sendPresenceSubscription(jid, Client.PresenceCommand.GRANT); } else { // ask user mControl.getViewControl().changed(new ViewEvent.SubscriptionRequest(contact)); } if (rawKey.length > 0) mControl.onPGPKey(contact, rawKey); } public void onPresenceUpdate(JID jid, Presence.Type type, Optional<String> optStatus) { JID myJID = mClient.getOwnJID().orElse(null); if (myJID != null && myJID.equals(jid)) // don't wanna see myself return; Contact contact = mModel.contacts().get(jid).orElse(null); if (contact == null) { LOGGER.info("can't find contact with jid: "+jid); return; } if (type == Presence.Type.available) { contact.setOnlineStatus(Contact.Online.YES); } else if (type == Presence.Type.unavailable) { contact.setOnlineStatus(Contact.Online.NO); } if (optStatus.isPresent()) contact.setStatusText(optStatus.get()); } public void onFingerprintPresence(JID jid, String fingerprint) { Contact contact = mModel.contacts().get(jid).orElse(null); if (contact == null) { LOGGER.info("can't find contact with jid: "+jid); return; } if (!fingerprint.isEmpty() && !fingerprint.equalsIgnoreCase(contact.getFingerprint())) { LOGGER.info("detected public key change, requesting new key..."); mControl.sendKeyRequest(contact); } } // TODO key IDs can be forged, searching by it is defective by design public void onSignaturePresence(JID jid, String signature) { Contact contact = mModel.contacts().get(jid).orElse(null); if (contact == null) { LOGGER.info("can't find contact with jid: "+jid); return; } long keyID = PGPUtils.parseKeyIDFromSignature(signature); if (keyID == 0) return; if (contact.hasKey()) { PGPUtils.PGPCoderKey key = Coder.contactkey(contact).orElse(null); if (key != null && key.signKey.getKeyID() == keyID) // already have this key return; } String id = Long.toHexString(keyID); HKPClient hkpClient = new HKPClient(); String foundKey = ""; for (String server: KEY_SERVERS) { foundKey = hkpClient.search(server, id); if (!foundKey.isEmpty()) break; } if (foundKey.isEmpty()) { LOGGER.config("searched for public key (nothing found): "+jid+" keyId="+id); return; } LOGGER.info("key found with HKP: "+jid+" keyId="+id); PGPUtils.PGPCoderKey key = PGPUtils.readPublicKey(foundKey).orElse(null); if (key == null) return; if (key.signKey.getKeyID() != keyID) { LOGGER.warning("key ID is not what we were searching for"); return; } mControl.getViewControl().changed(new ViewEvent.NewKey(contact, key)); } public void onPresenceError(JID jid, XMPPError.Type type, XMPPError.Condition condition) { if (type != XMPPError.Type.CANCEL) // it can't be that bad) return; Error error = null; switch (condition) { case remote_server_not_found: error = Error.SERVER_NOT_FOUND; } if (error == null) { LOGGER.warning("unhandled error condition: "+condition); return; } Contact contact = mModel.contacts().get(jid).orElse(null); if (contact == null) { LOGGER.info("can't find contact with jid: "+jid); return; } if (contact.getOnline() == Contact.Online.ERROR) // we already know this return; contact.setOnlineStatus(Contact.Online.ERROR); mControl.getViewControl().changed(new ViewEvent.PresenceError(contact, error)); } }