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
26package com.sun.jmx.remote.internal;
27
28import com.sun.jmx.remote.security.NotificationAccessController;
29import com.sun.jmx.remote.util.ClassLogger;
30import com.sun.jmx.remote.util.EnvHelp;
31import java.io.IOException;
32import java.security.AccessControlContext;
33import java.security.AccessController;
34import java.security.PrivilegedActionException;
35import java.security.PrivilegedExceptionAction;
36import java.util.ArrayList;
37import java.util.Collections;
38import java.util.HashMap;
39import java.util.HashSet;
40import java.util.List;
41import java.util.Map;
42import java.util.Set;
43import javax.management.InstanceNotFoundException;
44import javax.management.ListenerNotFoundException;
45import javax.management.MBeanPermission;
46import javax.management.MBeanServer;
47import javax.management.MBeanServerDelegate;
48import javax.management.MBeanServerNotification;
49import javax.management.Notification;
50import javax.management.NotificationBroadcaster;
51import javax.management.NotificationFilter;
52import javax.management.ObjectInstance;
53import javax.management.ObjectName;
54import javax.management.remote.NotificationResult;
55import javax.management.remote.TargetedNotification;
56import javax.management.MalformedObjectNameException;
57import javax.security.auth.Subject;
58
59public 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}
507