1 /*
   2  * Copyright (c) 2002, 2013, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.jmx.remote.internal;
  27 
  28 import com.sun.jmx.remote.security.NotificationAccessController;
  29 import com.sun.jmx.remote.util.ClassLogger;
  30 import com.sun.jmx.remote.util.EnvHelp;
  31 import java.io.IOException;
  32 import java.security.AccessControlContext;
  33 import java.security.AccessController;
  34 import java.security.PrivilegedActionException;
  35 import java.security.PrivilegedExceptionAction;
  36 import java.util.ArrayList;
  37 import java.util.Collections;
  38 import java.util.HashMap;
  39 import java.util.HashSet;
  40 import java.util.List;
  41 import java.util.Map;
  42 import java.util.Set;
  43 import javax.management.InstanceNotFoundException;
  44 import javax.management.ListenerNotFoundException;
  45 import javax.management.MBeanPermission;
  46 import javax.management.MBeanServer;
  47 import javax.management.MBeanServerDelegate;
  48 import javax.management.MBeanServerNotification;
  49 import javax.management.Notification;
  50 import javax.management.NotificationBroadcaster;
  51 import javax.management.NotificationFilter;
  52 import javax.management.ObjectInstance;
  53 import javax.management.ObjectName;
  54 import javax.management.remote.NotificationResult;
  55 import javax.management.remote.TargetedNotification;
  56 import javax.management.MalformedObjectNameException;
  57 import javax.security.auth.Subject;
  58 
  59 public class ServerNotifForwarder {
  60 
  61 
  62     public ServerNotifForwarder(MBeanServer mbeanServer,
  63                                 Map<String, ?> env,
  64                                 NotificationBuffer notifBuffer,
  65                                 String connectionId) {
  66         this.mbeanServer = mbeanServer;
  67         this.notifBuffer = notifBuffer;
  68         this.connectionId = connectionId;
  69         connectionTimeout = EnvHelp.getServerConnectionTimeout(env);
  70 
  71         String stringBoolean = (String) env.get("jmx.remote.x.check.notification.emission");
  72         checkNotificationEmission = EnvHelp.computeBooleanFromString( stringBoolean );
  73         notificationAccessController =
  74                 EnvHelp.getNotificationAccessController(env);
  75     }
  76 
  77     public Integer addNotificationListener(final ObjectName name,
  78         final NotificationFilter filter)
  79         throws InstanceNotFoundException, IOException {
  80 
  81         if (logger.traceOn()) {
  82             logger.trace("addNotificationListener",
  83                 "Add a listener at " + name);
  84         }
  85 
  86         checkState();
  87 
  88         // Explicitly check MBeanPermission for addNotificationListener
  89         //
  90         checkMBeanPermission(name, "addNotificationListener");
  91         if (notificationAccessController != null) {
  92             notificationAccessController.addNotificationListener(
  93                 connectionId, name, getSubject());
  94         }
  95         try {
  96             boolean instanceOf =
  97             AccessController.doPrivileged(
  98                     new PrivilegedExceptionAction<Boolean>() {
  99                         public Boolean run() throws InstanceNotFoundException {
 100                             return mbeanServer.isInstanceOf(name, broadcasterClass);
 101                         }
 102             });
 103             if (!instanceOf) {
 104                 throw new IllegalArgumentException("The specified MBean [" +
 105                     name + "] is not a " +
 106                     "NotificationBroadcaster " +
 107                     "object.");
 108             }
 109         } catch (PrivilegedActionException e) {
 110             throw (InstanceNotFoundException) extractException(e);
 111         }
 112 
 113         final Integer id = getListenerID();
 114 
 115         // 6238731: set the default domain if no domain is set.
 116         ObjectName nn = name;
 117         if (name.getDomain() == null || name.getDomain().equals("")) {
 118             try {
 119                 nn = ObjectName.getInstance(mbeanServer.getDefaultDomain(),
 120                                             name.getKeyPropertyList());
 121             } catch (MalformedObjectNameException mfoe) {
 122                 // impossible, but...
 123                 IOException ioe = new IOException(mfoe.getMessage());
 124                 ioe.initCause(mfoe);
 125                 throw ioe;
 126             }
 127         }
 128 
 129         synchronized (listenerMap) {
 130             IdAndFilter idaf = new IdAndFilter(id, filter);
 131             Set<IdAndFilter> set = listenerMap.get(nn);
 132             // Tread carefully because if set.size() == 1 it may be the
 133             // Collections.singleton we make here, which is unmodifiable.
 134             if (set == null)
 135                 set = Collections.singleton(idaf);
 136             else {
 137                 if (set.size() == 1)
 138                     set = new HashSet<IdAndFilter>(set);
 139                 set.add(idaf);
 140             }
 141             listenerMap.put(nn, set);
 142         }
 143 
 144         return id;
 145     }
 146 
 147     public void removeNotificationListener(ObjectName name,
 148         Integer[] listenerIDs)
 149         throws Exception {
 150 
 151         if (logger.traceOn()) {
 152             logger.trace("removeNotificationListener",
 153                 "Remove some listeners from " + name);
 154         }
 155 
 156         checkState();
 157 
 158         // Explicitly check MBeanPermission for removeNotificationListener
 159         //
 160         checkMBeanPermission(name, "removeNotificationListener");
 161         if (notificationAccessController != null) {
 162             notificationAccessController.removeNotificationListener(
 163                 connectionId, name, getSubject());
 164         }
 165 
 166         Exception re = null;
 167         for (int i = 0 ; i < listenerIDs.length ; i++) {
 168             try {
 169                 removeNotificationListener(name, listenerIDs[i]);
 170             } catch (Exception e) {
 171                 // Give back the first exception
 172                 //
 173                 if (re != null) {
 174                     re = e;
 175                 }
 176             }
 177         }
 178         if (re != null) {
 179             throw re;
 180         }
 181     }
 182 
 183     public void removeNotificationListener(ObjectName name, Integer listenerID)
 184     throws
 185         InstanceNotFoundException,
 186         ListenerNotFoundException,
 187         IOException {
 188 
 189         if (logger.traceOn()) {
 190             logger.trace("removeNotificationListener",
 191                 "Remove the listener " + listenerID + " from " + name);
 192         }
 193 
 194         checkState();
 195 
 196         if (name != null && !name.isPattern()) {
 197             if (!mbeanServer.isRegistered(name)) {
 198                 throw new InstanceNotFoundException("The MBean " + name +
 199                     " is not registered.");
 200             }
 201         }
 202 
 203         synchronized (listenerMap) {
 204             // Tread carefully because if set.size() == 1 it may be a
 205             // Collections.singleton, which is unmodifiable.
 206             Set<IdAndFilter> set = listenerMap.get(name);
 207             IdAndFilter idaf = new IdAndFilter(listenerID, null);
 208             if (set == null || !set.contains(idaf))
 209                 throw new ListenerNotFoundException("Listener not found");
 210             if (set.size() == 1)
 211                 listenerMap.remove(name);
 212             else
 213                 set.remove(idaf);
 214         }
 215     }
 216 
 217     /* This is the object that will apply our filtering to candidate
 218      * notifications.  First of all, if there are no listeners for the
 219      * ObjectName that the notification is coming from, we go no further.
 220      * Then, for each listener, we must apply the corresponding filter (if any)
 221      * and ignore the listener if the filter rejects.  Finally, we apply
 222      * some access checks which may also reject the listener.
 223      *
 224      * A given notification may trigger several listeners on the same MBean,
 225      * which is why listenerMap is a Map<ObjectName, Set<IdAndFilter>> and
 226      * why we add the found notifications to a supplied List rather than
 227      * just returning a boolean.
 228      */
 229     private final NotifForwarderBufferFilter bufferFilter = new NotifForwarderBufferFilter();
 230 
 231     final class NotifForwarderBufferFilter implements NotificationBufferFilter {
 232         public void apply(List<TargetedNotification> targetedNotifs,
 233                           ObjectName source, Notification notif) {
 234             // We proceed in two stages here, to avoid holding the listenerMap
 235             // lock while invoking the filters (which are user code).
 236             final IdAndFilter[] candidates;
 237             synchronized (listenerMap) {
 238                 final Set<IdAndFilter> set = listenerMap.get(source);
 239                 if (set == null) {
 240                     logger.debug("bufferFilter", "no listeners for this name");
 241                     return;
 242                 }
 243                 candidates = new IdAndFilter[set.size()];
 244                 set.toArray(candidates);
 245             }
 246             // We don't synchronize on targetedNotifs, because it is a local
 247             // variable of our caller and no other thread can see it.
 248             for (IdAndFilter idaf : candidates) {
 249                 final NotificationFilter nf = idaf.getFilter();
 250                 if (nf == null || nf.isNotificationEnabled(notif)) {
 251                     logger.debug("bufferFilter", "filter matches");
 252                     final TargetedNotification tn =
 253                             new TargetedNotification(notif, idaf.getId());
 254                     if (allowNotificationEmission(source, tn))
 255                         targetedNotifs.add(tn);
 256                 }
 257             }
 258         }
 259     };
 260 
 261     public NotificationResult fetchNotifs(long startSequenceNumber,
 262         long timeout,
 263         int maxNotifications) {
 264         if (logger.traceOn()) {
 265             logger.trace("fetchNotifs", "Fetching notifications, the " +
 266                 "startSequenceNumber is " + startSequenceNumber +
 267                 ", the timeout is " + timeout +
 268                 ", the maxNotifications is " + maxNotifications);
 269         }
 270 
 271         NotificationResult nr;
 272         final long t = Math.min(connectionTimeout, timeout);
 273         try {
 274             nr = notifBuffer.fetchNotifications(bufferFilter,
 275                 startSequenceNumber,
 276                 t, maxNotifications);
 277             snoopOnUnregister(nr);
 278         } catch (InterruptedException ire) {
 279             nr = new NotificationResult(0L, 0L, new TargetedNotification[0]);
 280         }
 281 
 282         if (logger.traceOn()) {
 283             logger.trace("fetchNotifs", "Forwarding the notifs: "+nr);
 284         }
 285 
 286         return nr;
 287     }
 288 
 289     // The standard RMI connector client will register a listener on the MBeanServerDelegate
 290     // in order to be told when MBeans are unregistered.  We snoop on fetched notifications
 291     // so that we can know too, and remove the corresponding entry from the listenerMap.
 292     // See 6957378.
 293     private void snoopOnUnregister(NotificationResult nr) {
 294         List<IdAndFilter> copy = null;
 295         synchronized (listenerMap) {
 296             Set<IdAndFilter> delegateSet = listenerMap.get(MBeanServerDelegate.DELEGATE_NAME);
 297             if (delegateSet == null || delegateSet.isEmpty()) {
 298                 return;
 299             }
 300             copy = new ArrayList<>(delegateSet);
 301         }
 302 
 303         for (TargetedNotification tn : nr.getTargetedNotifications()) {
 304             Integer id = tn.getListenerID();
 305             for (IdAndFilter idaf : copy) {
 306                 if (idaf.id == id) {
 307                     // This is a notification from the MBeanServerDelegate.
 308                     Notification n = tn.getNotification();
 309                     if (n instanceof MBeanServerNotification &&
 310                             n.getType().equals(MBeanServerNotification.UNREGISTRATION_NOTIFICATION)) {
 311                         MBeanServerNotification mbsn = (MBeanServerNotification) n;
 312                         ObjectName gone = mbsn.getMBeanName();
 313                         synchronized (listenerMap) {
 314                             listenerMap.remove(gone);
 315                         }
 316                     }
 317                 }
 318             }
 319         }
 320     }
 321 
 322     public void terminate() {
 323         if (logger.traceOn()) {
 324             logger.trace("terminate", "Be called.");
 325         }
 326 
 327         synchronized(terminationLock) {
 328             if (terminated) {
 329                 return;
 330             }
 331 
 332             terminated = true;
 333 
 334             synchronized(listenerMap) {
 335                 listenerMap.clear();
 336             }
 337         }
 338 
 339         if (logger.traceOn()) {
 340             logger.trace("terminate", "Terminated.");
 341         }
 342     }
 343 
 344     //----------------
 345     // PRIVATE METHODS
 346     //----------------
 347 
 348     private Subject getSubject() {
 349         return Subject.getSubject(AccessController.getContext());
 350     }
 351 
 352     private void checkState() throws IOException {
 353         synchronized(terminationLock) {
 354             if (terminated) {
 355                 throw new IOException("The connection has been terminated.");
 356             }
 357         }
 358     }
 359 
 360     private Integer getListenerID() {
 361         synchronized(listenerCounterLock) {
 362             return listenerCounter++;
 363         }
 364     }
 365 
 366     /**
 367      * Explicitly check the MBeanPermission for
 368      * the current access control context.
 369      */
 370     public final void checkMBeanPermission(
 371             final ObjectName name, final String actions)
 372             throws InstanceNotFoundException, SecurityException {
 373         checkMBeanPermission(mbeanServer,name,actions);
 374     }
 375 
 376     static void checkMBeanPermission(
 377             final MBeanServer mbs, final ObjectName name, final String actions)
 378             throws InstanceNotFoundException, SecurityException {
 379 
 380         SecurityManager sm = System.getSecurityManager();
 381         if (sm != null) {
 382             AccessControlContext acc = AccessController.getContext();
 383             ObjectInstance oi;
 384             try {
 385                 oi = AccessController.doPrivileged(
 386                     new PrivilegedExceptionAction<ObjectInstance>() {
 387                         public ObjectInstance run()
 388                         throws InstanceNotFoundException {
 389                             return mbs.getObjectInstance(name);
 390                         }
 391                 });
 392             } catch (PrivilegedActionException e) {
 393                 throw (InstanceNotFoundException) extractException(e);
 394             }
 395             String classname = oi.getClassName();
 396             MBeanPermission perm = new MBeanPermission(
 397                 classname,
 398                 null,
 399                 name,
 400                 actions);
 401             sm.checkPermission(perm, acc);
 402         }
 403     }
 404 
 405     /**
 406      * Check if the caller has the right to get the following notifications.
 407      */
 408     private boolean allowNotificationEmission(ObjectName name,
 409                                               TargetedNotification tn) {
 410         try {
 411             if (checkNotificationEmission) {
 412                 checkMBeanPermission(name, "addNotificationListener");
 413             }
 414             if (notificationAccessController != null) {
 415                 notificationAccessController.fetchNotification(
 416                         connectionId, name, tn.getNotification(), getSubject());
 417             }
 418             return true;
 419         } catch (SecurityException e) {
 420             if (logger.debugOn()) {
 421                 logger.debug("fetchNotifs", "Notification " +
 422                         tn.getNotification() + " not forwarded: the " +
 423                         "caller didn't have the required access rights");
 424             }
 425             return false;
 426         } catch (Exception e) {
 427             if (logger.debugOn()) {
 428                 logger.debug("fetchNotifs", "Notification " +
 429                         tn.getNotification() + " not forwarded: " +
 430                         "got an unexpected exception: " + e);
 431             }
 432             return false;
 433         }
 434     }
 435 
 436     /**
 437      * Iterate until we extract the real exception
 438      * from a stack of PrivilegedActionExceptions.
 439      */
 440     private static Exception extractException(Exception e) {
 441         while (e instanceof PrivilegedActionException) {
 442             e = ((PrivilegedActionException)e).getException();
 443         }
 444         return e;
 445     }
 446 
 447     private static class IdAndFilter {
 448         private Integer id;
 449         private NotificationFilter filter;
 450 
 451         IdAndFilter(Integer id, NotificationFilter filter) {
 452             this.id = id;
 453             this.filter = filter;
 454         }
 455 
 456         Integer getId() {
 457             return this.id;
 458         }
 459 
 460         NotificationFilter getFilter() {
 461             return this.filter;
 462         }
 463 
 464         @Override
 465         public int hashCode() {
 466             return id.hashCode();
 467         }
 468 
 469         @Override
 470         public boolean equals(Object o) {
 471             return ((o instanceof IdAndFilter) &&
 472                     ((IdAndFilter) o).getId().equals(getId()));
 473         }
 474     }
 475 
 476 
 477     //------------------
 478     // PRIVATE VARIABLES
 479     //------------------
 480 
 481     private MBeanServer mbeanServer;
 482 
 483     private final String connectionId;
 484 
 485     private final long connectionTimeout;
 486 
 487     private static int listenerCounter = 0;
 488     private final static int[] listenerCounterLock = new int[0];
 489 
 490     private NotificationBuffer notifBuffer;
 491     private final Map<ObjectName, Set<IdAndFilter>> listenerMap =
 492             new HashMap<ObjectName, Set<IdAndFilter>>();
 493 
 494     private boolean terminated = false;
 495     private final int[] terminationLock = new int[0];
 496 
 497     static final String broadcasterClass =
 498         NotificationBroadcaster.class.getName();
 499 
 500     private final boolean checkNotificationEmission;
 501 
 502     private final NotificationAccessController notificationAccessController;
 503 
 504     private static final ClassLogger logger =
 505         new ClassLogger("javax.management.remote.misc", "ServerNotifForwarder");
 506 }