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().isEmpty()) { 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 }