CarbonManager.java

  1. /**
  2.  *
  3.  * Copyright 2013-2014 Georg Lukas, 2017-2020 Florian Schmaus, 2020 Paul Schaub
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.jivesoftware.smackx.carbons;

  18. import java.util.Map;
  19. import java.util.Set;
  20. import java.util.WeakHashMap;
  21. import java.util.concurrent.CopyOnWriteArraySet;
  22. import java.util.logging.Level;
  23. import java.util.logging.Logger;

  24. import org.jivesoftware.smack.AsyncButOrdered;
  25. import org.jivesoftware.smack.ConnectionCreationListener;
  26. import org.jivesoftware.smack.ConnectionListener;
  27. import org.jivesoftware.smack.Manager;
  28. import org.jivesoftware.smack.SmackException;
  29. import org.jivesoftware.smack.SmackException.NoResponseException;
  30. import org.jivesoftware.smack.SmackException.NotConnectedException;
  31. import org.jivesoftware.smack.SmackFuture;
  32. import org.jivesoftware.smack.StanzaListener;
  33. import org.jivesoftware.smack.XMPPConnection;
  34. import org.jivesoftware.smack.XMPPConnectionRegistry;
  35. import org.jivesoftware.smack.XMPPException;
  36. import org.jivesoftware.smack.XMPPException.XMPPErrorException;
  37. import org.jivesoftware.smack.filter.AndFilter;
  38. import org.jivesoftware.smack.filter.FromMatchesFilter;
  39. import org.jivesoftware.smack.filter.OrFilter;
  40. import org.jivesoftware.smack.filter.StanzaExtensionFilter;
  41. import org.jivesoftware.smack.filter.StanzaFilter;
  42. import org.jivesoftware.smack.filter.StanzaTypeFilter;
  43. import org.jivesoftware.smack.packet.IQ;
  44. import org.jivesoftware.smack.packet.Message;
  45. import org.jivesoftware.smack.packet.Stanza;
  46. import org.jivesoftware.smack.util.ExceptionCallback;
  47. import org.jivesoftware.smack.util.SuccessCallback;

  48. import org.jivesoftware.smackx.carbons.packet.Carbon;
  49. import org.jivesoftware.smackx.carbons.packet.CarbonExtension;
  50. import org.jivesoftware.smackx.carbons.packet.CarbonExtension.Direction;
  51. import org.jivesoftware.smackx.carbons.packet.CarbonExtension.Private;
  52. import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
  53. import org.jivesoftware.smackx.forward.packet.Forwarded;

  54. import org.jxmpp.jid.BareJid;
  55. import org.jxmpp.jid.EntityFullJid;

  56. /**
  57.  * Manager for XEP-0280: Message Carbons. This class implements the manager for registering {@link CarbonExtension}
  58.  * support, enabling and disabling message carbons, and for {@link CarbonCopyReceivedListener}.
  59.  * <p>
  60.  * Note that <b>it is important to match the 'from' attribute of the message wrapping a carbon copy</b>, as otherwise it would
  61.  * may be possible for others to impersonate users. Smack's CarbonManager takes care of that in
  62.  * {@link CarbonCopyReceivedListener}s which were registered with
  63.  * {@link #addCarbonCopyReceivedListener(CarbonCopyReceivedListener)}.
  64.  * </p>
  65.  * <p>
  66.  * You should call enableCarbons() before sending your first undirected presence (aka. the "initial presence").
  67.  * </p>
  68.  *
  69.  * @author Georg Lukas
  70.  * @author Florian Schmaus
  71.  * @author Paul Schaub
  72.  */
  73. public final class CarbonManager extends Manager {

  74.     private static final Logger LOGGER = Logger.getLogger(CarbonManager.class.getName());
  75.     private static Map<XMPPConnection, CarbonManager> INSTANCES = new WeakHashMap<>();

  76.     private static boolean ENABLED_BY_DEFAULT = false;

  77.     static {
  78.         XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
  79.             @Override
  80.             public void connectionCreated(XMPPConnection connection) {
  81.                 getInstanceFor(connection);
  82.             }
  83.         });
  84.     }

  85.     private static final StanzaFilter CARBON_EXTENSION_FILTER =
  86.                     // @formatter:off
  87.                     new AndFilter(
  88.                         new OrFilter(
  89.                             new StanzaExtensionFilter(CarbonExtension.Direction.sent.name(), CarbonExtension.NAMESPACE),
  90.                             new StanzaExtensionFilter(CarbonExtension.Direction.received.name(), CarbonExtension.NAMESPACE)
  91.                         ),
  92.                         StanzaTypeFilter.MESSAGE
  93.                     );
  94.                     // @formatter:on

  95.     private final Set<CarbonCopyReceivedListener> listeners = new CopyOnWriteArraySet<>();

  96.     private volatile boolean enabled_state = false;
  97.     private volatile boolean enabledByDefault = ENABLED_BY_DEFAULT;

  98.     private final StanzaListener carbonsListener;

  99.     private final AsyncButOrdered<BareJid> carbonsListenerAsyncButOrdered = new AsyncButOrdered<>();

  100.     /**
  101.      * Should Carbons be automatically be enabled once the connection is authenticated?
  102.      * Default: false
  103.      *
  104.      * @param enabledByDefault new default value
  105.      */
  106.     public static void setEnabledByDefault(boolean enabledByDefault) {
  107.         ENABLED_BY_DEFAULT = enabledByDefault;
  108.     }

  109.     private CarbonManager(XMPPConnection connection) {
  110.         super(connection);
  111.         ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection);
  112.         sdm.addFeature(CarbonExtension.NAMESPACE);

  113.         carbonsListener = new StanzaListener() {
  114.             @Override
  115.             public void processStanza(final Stanza stanza) {
  116.                 final Message wrappingMessage = (Message) stanza;
  117.                 final CarbonExtension carbonExtension = CarbonExtension.from(wrappingMessage);
  118.                 final Direction direction = carbonExtension.getDirection();
  119.                 final Forwarded<Message> forwarded = carbonExtension.getForwarded();
  120.                 final Message carbonCopy = forwarded.getForwardedStanza();
  121.                 final BareJid from = carbonCopy.getFrom().asBareJid();

  122.                 carbonsListenerAsyncButOrdered.performAsyncButOrdered(from, new Runnable() {
  123.                     @Override
  124.                     public void run() {
  125.                         for (CarbonCopyReceivedListener listener : listeners) {
  126.                             listener.onCarbonCopyReceived(direction, carbonCopy, wrappingMessage);
  127.                         }
  128.                     }
  129.                 });
  130.             }
  131.         };

  132.         connection.addConnectionListener(new ConnectionListener() {
  133.             @Override
  134.             public void connectionClosed() {
  135.                 // Reset the state if the connection was cleanly closed. Note that this is not strictly necessary,
  136.                 // because we also reset in authenticated() if the stream got not resumed, but for maximum correctness,
  137.                 // also reset here.
  138.                 enabled_state = false;
  139.                 connection().removeSyncStanzaListener(carbonsListener);
  140.             }
  141.             @Override
  142.             public void authenticated(XMPPConnection connection, boolean resumed) {
  143.                 if (!resumed) {
  144.                     // Non-resumed XMPP sessions always start with disabled carbons
  145.                     enabled_state = false;
  146.                     try {
  147.                         if (shouldCarbonsBeEnabled() && isSupportedByServer()) {
  148.                             setCarbonsEnabled(true);
  149.                         }
  150.                     } catch (InterruptedException | XMPPErrorException | NotConnectedException | NoResponseException e) {
  151.                         LOGGER.log(Level.WARNING, "Cannot check for Carbon support and / or enable carbons.", e);
  152.                     }
  153.                 }
  154.                 addCarbonsListener(connection);
  155.             }
  156.         });

  157.         addCarbonsListener(connection);
  158.     }

  159.     private void addCarbonsListener(XMPPConnection connection) {
  160.         EntityFullJid localAddress = connection.getUser();
  161.         if (localAddress == null) {
  162.             // We where not connected yet and thus we don't know our XMPP address at the moment, which we need to match incoming
  163.             // carbons securely. Abort here. The ConnectionListener above will eventually setup the carbons listener.
  164.             return;
  165.         }

  166.         // XEP-0280 ยง 11. Security Considerations "Any forwarded copies received by a Carbons-enabled client MUST be
  167.         // from that user's bare JID; any copies that do not meet this requirement MUST be ignored." Otherwise, if
  168.         // those copies do not get ignored, malicious users may be able to impersonate other users. That is why the
  169.         // 'from' matcher is important here.
  170.         connection.addSyncStanzaListener(carbonsListener, new AndFilter(CARBON_EXTENSION_FILTER,
  171.                         FromMatchesFilter.createBare(localAddress)));
  172.     }

  173.     /**
  174.      * Obtain the CarbonManager responsible for a connection.
  175.      *
  176.      * @param connection the connection object.
  177.      *
  178.      * @return a CarbonManager instance
  179.      */
  180.     public static synchronized CarbonManager getInstanceFor(XMPPConnection connection) {
  181.         CarbonManager carbonManager = INSTANCES.get(connection);

  182.         if (carbonManager == null) {
  183.             carbonManager = new CarbonManager(connection);
  184.             INSTANCES.put(connection, carbonManager);
  185.         }

  186.         return carbonManager;
  187.     }

  188.     private static IQ carbonsEnabledIQ(final boolean new_state) {
  189.         IQ request;
  190.         if (new_state) {
  191.             request = new Carbon.Enable();
  192.         } else {
  193.             request = new Carbon.Disable();
  194.         }
  195.         return request;
  196.     }

  197.     /**
  198.      * Add a carbon copy received listener.
  199.      *
  200.      * @param listener the listener to register.
  201.      * @return <code>true</code> if the filter was not already registered.
  202.      * @since 4.2
  203.      */
  204.     public boolean addCarbonCopyReceivedListener(CarbonCopyReceivedListener listener) {
  205.         return listeners.add(listener);
  206.     }

  207.     /**
  208.      * Remove a carbon copy received listener.
  209.      *
  210.      * @param listener the listener to register.
  211.      * @return <code>true</code> if the filter was registered.
  212.      * @since 4.2
  213.      */
  214.     public boolean removeCarbonCopyReceivedListener(CarbonCopyReceivedListener listener) {
  215.         return listeners.remove(listener);
  216.     }

  217.     /**
  218.      * Returns true if XMPP Carbons are supported by the server.
  219.      *
  220.      * @return true if supported
  221.      * @throws NotConnectedException if the XMPP connection is not connected.
  222.      * @throws XMPPErrorException if there was an XMPP error returned.
  223.      * @throws NoResponseException if there was no response from the remote entity.
  224.      * @throws InterruptedException if the calling thread was interrupted.
  225.      */
  226.     public boolean isSupportedByServer() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  227.         return ServiceDiscoveryManager.getInstanceFor(connection()).serverSupportsFeature(CarbonExtension.NAMESPACE);
  228.     }

  229.     /**
  230.      * Notify server to change the carbons state. This method returns
  231.      * immediately and changes the variable when the reply arrives.
  232.      *
  233.      * You should first check for support using isSupportedByServer().
  234.      *
  235.      * @param new_state whether carbons should be enabled or disabled
  236.      * @deprecated use {@link #enableCarbonsAsync(ExceptionCallback)} or {@link #disableCarbonsAsync(ExceptionCallback)} instead.
  237.      */
  238.     @Deprecated
  239.     public void sendCarbonsEnabled(final boolean new_state) {
  240.         sendUseCarbons(new_state, null);
  241.     }

  242.     /**
  243.      * Enable carbons asynchronously. If an error occurs as result of the attempt to enable carbons, the optional
  244.      * <code>exceptionCallback</code> will be invoked.
  245.      * <p>
  246.      * Note that although this method is asynchronous, it may block if the outgoing stream element queue is full (e.g.
  247.      * because of a slow network connection). Thus, if the thread performing this operation is interrupted while the
  248.      * queue is full, an {@link InterruptedException} is thrown.
  249.      * </p>
  250.      *
  251.      * @param exceptionCallback the optional exception callback.
  252.      * @since 4.2
  253.      */
  254.     public void enableCarbonsAsync(ExceptionCallback<Exception> exceptionCallback) {
  255.         sendUseCarbons(true, exceptionCallback);
  256.     }

  257.     /**
  258.      * Disable carbons asynchronously. If an error occurs as result of the attempt to disable carbons, the optional
  259.      * <code>exceptionCallback</code> will be invoked.
  260.      * <p>
  261.      * Note that although this method is asynchronous, it may block if the outgoing stream element queue is full (e.g.
  262.      * because of a slow network connection). Thus, if the thread performing this operation is interrupted while the
  263.      * queue is full, an {@link InterruptedException} is thrown.
  264.      * </p>
  265.      *
  266.      * @param exceptionCallback the optional exception callback.
  267.      * @since 4.2
  268.      */
  269.     public void disableCarbonsAsync(ExceptionCallback<Exception> exceptionCallback) {
  270.         sendUseCarbons(false, exceptionCallback);
  271.     }

  272.     private void sendUseCarbons(final boolean use, ExceptionCallback<Exception> exceptionCallback) {
  273.         enabledByDefault = use;
  274.         IQ setIQ = carbonsEnabledIQ(use);

  275.         SmackFuture<IQ, Exception> future = connection().sendIqRequestAsync(setIQ);

  276.         future.onSuccess(new SuccessCallback<IQ>() {

  277.             @Override
  278.             public void onSuccess(IQ result) {
  279.                 enabled_state = use;
  280.             }
  281.         }).onError(exceptionCallback);
  282.     }

  283.     /**
  284.      * Notify server to change the carbons state. This method blocks
  285.      * some time until the server replies to the IQ and returns true on
  286.      * success.
  287.      *
  288.      * You should first check for support using isSupportedByServer().
  289.      *
  290.      * @param new_state whether carbons should be enabled or disabled
  291.      * @throws XMPPErrorException if there was an XMPP error returned.
  292.      * @throws NoResponseException if there was no response from the remote entity.
  293.      * @throws NotConnectedException if the XMPP connection is not connected.
  294.      * @throws InterruptedException if the calling thread was interrupted.
  295.      *
  296.      */
  297.     public synchronized void setCarbonsEnabled(final boolean new_state) throws NoResponseException,
  298.                     XMPPErrorException, NotConnectedException, InterruptedException {
  299.         enabledByDefault = new_state;
  300.         if (enabled_state == new_state)
  301.             return;

  302.         IQ setIQ = carbonsEnabledIQ(new_state);

  303.         connection().createStanzaCollectorAndSend(setIQ).nextResultOrThrow();
  304.         enabled_state = new_state;
  305.     }

  306.     /**
  307.      * Helper method to enable carbons.
  308.      *
  309.      * @throws XMPPException if an XMPP protocol error was received.
  310.      * @throws SmackException if there was no response from the server.
  311.      * @throws InterruptedException if the calling thread was interrupted.
  312.      */
  313.     public void enableCarbons() throws XMPPException, SmackException, InterruptedException {
  314.         setCarbonsEnabled(true);
  315.     }

  316.     /**
  317.      * Helper method to disable carbons.
  318.      *
  319.      * @throws XMPPException if an XMPP protocol error was received.
  320.      * @throws SmackException if there was no response from the server.
  321.      * @throws InterruptedException if the calling thread was interrupted.
  322.      */
  323.     public void disableCarbons() throws XMPPException, SmackException, InterruptedException {
  324.         setCarbonsEnabled(false);
  325.     }

  326.     /**
  327.      * Check if carbons are enabled on this connection.
  328.      *
  329.      * @return true if carbons are enabled, else false.
  330.      */
  331.     public boolean getCarbonsEnabled() {
  332.         return this.enabled_state;
  333.     }

  334.     private boolean shouldCarbonsBeEnabled() {
  335.         return enabledByDefault;
  336.     }

  337.     /**
  338.      * Mark a message as "private", so it will not be carbon-copied.
  339.      *
  340.      * @param msg Message object to mark private
  341.      * @deprecated use {@link Private#addTo(Message)}
  342.      */
  343.     @Deprecated
  344.     public static void disableCarbons(Message msg) {
  345.         msg.addExtension(Private.INSTANCE);
  346.     }
  347. }