src/jdk.management.rest/share/classes/jdk/internal/management/remote/rest/http/MBeanServerResource.java
branchjmx-rest-api
changeset 56026 bd531f08d7c7
equal deleted inserted replaced
56007:d6cbabcaf518 56026:bd531f08d7c7
       
     1 /*
       
     2  * Copyright (c) 2018, 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 jdk.internal.management.remote.rest.http;
       
    27 
       
    28 import jdk.internal.management.remote.rest.json.JSONElement;
       
    29 import jdk.internal.management.remote.rest.mapper.JSONMapper;
       
    30 import jdk.internal.management.remote.rest.mapper.JSONMappingException;
       
    31 import jdk.internal.management.remote.rest.mapper.JSONMappingFactory;
       
    32 import com.sun.jmx.remote.security.JMXPluggableAuthenticator;
       
    33 import com.sun.jmx.remote.security.JMXSubjectDomainCombiner;
       
    34 import com.sun.jmx.remote.security.SubjectDelegator;
       
    35 import com.sun.net.httpserver.BasicAuthenticator;
       
    36 import com.sun.net.httpserver.HttpContext;
       
    37 import com.sun.net.httpserver.HttpExchange;
       
    38 import com.sun.net.httpserver.HttpServer;
       
    39 
       
    40 import javax.management.JMX;
       
    41 import javax.management.MBeanServer;
       
    42 import javax.management.MBeanServerDelegate;
       
    43 import javax.management.MBeanServerDelegateMBean;
       
    44 import javax.management.remote.JMXAuthenticator;
       
    45 import jdk.internal.management.remote.rest.JmxRestAdapter;
       
    46 import jdk.internal.management.remote.rest.PlatformRestAdapter;
       
    47 
       
    48 import javax.security.auth.Subject;
       
    49 import java.io.IOException;
       
    50 import java.lang.reflect.InvocationHandler;
       
    51 import java.lang.reflect.Method;
       
    52 import java.lang.reflect.Proxy;
       
    53 import java.net.URLDecoder;
       
    54 import java.nio.charset.StandardCharsets;
       
    55 import java.security.AccessControlContext;
       
    56 import java.security.AccessController;
       
    57 import java.security.PrivilegedAction;
       
    58 import java.util.Arrays;
       
    59 import java.util.LinkedHashMap;
       
    60 import java.util.Map;
       
    61 import java.util.concurrent.ConcurrentHashMap;
       
    62 import java.util.concurrent.TimeUnit;
       
    63 import java.util.concurrent.atomic.AtomicInteger;
       
    64 
       
    65 
       
    66 public final class MBeanServerResource implements RestResource, JmxRestAdapter {
       
    67 
       
    68     // Initialization parameters
       
    69     private final HttpServer httpServer;
       
    70     private final String contextStr;
       
    71     private final Map<String, ?> env;
       
    72     private final MBeanServer mbeanServer;
       
    73 
       
    74     // Save the context to start/stop the adapter
       
    75     private HttpContext httpContext;
       
    76     private JMXAuthenticator authenticator = null;
       
    77     private final MBeanServerDelegateMBean mBeanServerDelegateMBean;
       
    78 
       
    79     // Save MBeanServer Proxy for a user
       
    80     private final MBeanCollectionResource defaultMBeansResource;
       
    81     // Use an expiring map that removes entries after the configured period lapses.
       
    82     private final TimedMap<String, MBeanCollectionResource> proxyMBeanServers = new TimedMap<>(5*60);
       
    83 
       
    84     private static AtomicInteger resourceNumber = new AtomicInteger(1);
       
    85     private boolean started = false;
       
    86 
       
    87     public MBeanServerResource(HttpServer hServer, MBeanServer mbeanServer,
       
    88                                String context, Map<String, ?> env) {
       
    89         this.httpServer = hServer;
       
    90         this.env = env;
       
    91         this.mbeanServer = mbeanServer;
       
    92 
       
    93         mBeanServerDelegateMBean = JMX.newMBeanProxy(mbeanServer,
       
    94                 MBeanServerDelegate.DELEGATE_NAME, MBeanServerDelegateMBean.class);
       
    95 
       
    96         if (context == null || context.isEmpty()) {
       
    97             contextStr = "server-" + resourceNumber.getAndIncrement();
       
    98         } else {
       
    99             contextStr = context;
       
   100         }
       
   101         // setup authentication
       
   102         if (env.get("jmx.remote.x.authentication") != null) {
       
   103             authenticator = (JMXAuthenticator) env.get("jmx.remote.authenticator");
       
   104             if (authenticator == null) {
       
   105                 if (env.get("jmx.remote.x.password.file") != null
       
   106                         || env.get("jmx.remote.x.login.config") != null) {
       
   107                     authenticator = new JMXPluggableAuthenticator(env);
       
   108                 } else {
       
   109                     throw new IllegalArgumentException
       
   110                             ("Config error : Authentication is enabled with no authenticator");
       
   111                 }
       
   112             }
       
   113         }
       
   114 
       
   115         if (env.get("jmx.remote.x.authentication") == null) {
       
   116             defaultMBeansResource = new MBeanCollectionResource(mbeanServer);
       
   117         } else {
       
   118             defaultMBeansResource = null;
       
   119         }
       
   120     }
       
   121 
       
   122     private MBeanServer getMBeanServerProxy(MBeanServer mbeaServer, Subject subject) {
       
   123         return (MBeanServer) Proxy.newProxyInstance(MBeanServer.class.getClassLoader(),
       
   124                 new Class<?>[]{MBeanServer.class},
       
   125                 new AuthInvocationHandler(mbeaServer, subject));
       
   126     }
       
   127 
       
   128     @Override
       
   129     public HttpResponse doGet(HttpExchange exchange) {
       
   130         String selfUrl = getUrl();
       
   131         Map<String, String> links = new LinkedHashMap<>();
       
   132         links.put("mbeans", selfUrl + "/mbeans");
       
   133 
       
   134         Map<String, Object> mBeanServerInfo = getMBeanServerInfo();
       
   135         mBeanServerInfo.put("_links", links);
       
   136 
       
   137         final JSONMapper typeMapper = JSONMappingFactory.INSTANCE.getTypeMapper(mBeanServerInfo);
       
   138         if (typeMapper != null) {
       
   139             try {
       
   140                 JSONElement jsonElement = typeMapper.toJsonValue(mBeanServerInfo);
       
   141                 return new HttpResponse(jsonElement.toJsonString());
       
   142             } catch (JSONMappingException e) {
       
   143                 return HttpResponse.SERVER_ERROR;
       
   144             }
       
   145         } else {
       
   146             return HttpResponse.SERVER_ERROR;
       
   147         }
       
   148     }
       
   149 
       
   150     @Override
       
   151     public void handle(HttpExchange exchange) throws IOException {
       
   152         MBeanCollectionResource mBeansResource = defaultMBeansResource;
       
   153         if (env.get("jmx.remote.x.authentication") != null) {
       
   154             String authCredentials = HttpUtil.getCredentials(exchange);
       
   155             // MBeanServer proxy should be populated in the authenticator
       
   156             mBeansResource = proxyMBeanServers.get(authCredentials);
       
   157             if (mBeansResource == null) {
       
   158                 throw new IllegalArgumentException("Invalid HTTP request Headers");
       
   159             }
       
   160         }
       
   161 
       
   162         String path = URLDecoder.decode(exchange.getRequestURI().getPath(), StandardCharsets.UTF_8.displayName());
       
   163         String pathPrefix = httpContext.getPath();
       
   164         // Route request to appropriate resource
       
   165         if (path.matches(pathPrefix + "/?$")) {
       
   166             RestResource.super.handle(exchange);
       
   167         } else if (path.matches(pathPrefix + "/mbeans.*")) {
       
   168             mBeansResource.handle(exchange);
       
   169         } else {
       
   170             HttpUtil.sendResponse(exchange, HttpResponse.REQUEST_NOT_FOUND);
       
   171         }
       
   172     }
       
   173 
       
   174     @Override
       
   175     public synchronized void start() {
       
   176         if (!started) {
       
   177             httpContext = httpServer.createContext("/jmx/servers/" + contextStr, this);
       
   178             if (env.get("jmx.remote.x.authentication") != null) {
       
   179                 httpContext.setAuthenticator(new RestAuthenticator("jmx-rest"));
       
   180             }
       
   181             started = true;
       
   182         }
       
   183     }
       
   184 
       
   185     @Override
       
   186     public synchronized void stop() {
       
   187         if (!started) {
       
   188             throw new IllegalStateException("Rest Adapter not started yet");
       
   189         }
       
   190         httpServer.removeContext(httpContext);
       
   191         started = false;
       
   192     }
       
   193 
       
   194     @Override
       
   195     public String getUrl() {
       
   196         return PlatformRestAdapter.getBaseURL() + "/" + contextStr;
       
   197     }
       
   198 
       
   199     @Override
       
   200     public MBeanServer getMBeanServer() {
       
   201         return mbeanServer;
       
   202     }
       
   203 
       
   204     public String getContext() {
       
   205         return contextStr;
       
   206     }
       
   207 
       
   208     private class RestAuthenticator extends BasicAuthenticator {
       
   209 
       
   210         RestAuthenticator(String realm) {
       
   211             super(realm);
       
   212         }
       
   213 
       
   214         @Override
       
   215         public boolean checkCredentials(String username, String password) {
       
   216             if (proxyMBeanServers.containsKey(username)) {
       
   217                 return true;
       
   218             } else {
       
   219                 Subject subject = null;
       
   220                 if (authenticator != null) {
       
   221                     String[] credential = new String[]{username, password};
       
   222                     try {
       
   223                         subject = authenticator.authenticate(credential);
       
   224                     } catch (SecurityException e) {
       
   225                         return false;
       
   226                     }
       
   227                 }
       
   228                 MBeanServer proxy = getMBeanServerProxy(mbeanServer, subject);
       
   229                 proxyMBeanServers.put(username, new MBeanCollectionResource(proxy));
       
   230                 return true;
       
   231             }
       
   232         }
       
   233     }
       
   234 
       
   235     Map<String, Object> getMBeanServerInfo() {
       
   236         Map<String, Object> result = new LinkedHashMap<>();
       
   237 
       
   238         result.put("id", mBeanServerDelegateMBean.getMBeanServerId());
       
   239         result.put("context", contextStr);
       
   240 
       
   241         result.put("defaultDomain", mbeanServer.getDefaultDomain());
       
   242         result.put("mBeanCount", mbeanServer.getMBeanCount());
       
   243         result.put("domains", Arrays.toString(mbeanServer.getDomains()));
       
   244 
       
   245         result.put("specName", mBeanServerDelegateMBean.getSpecificationName());
       
   246         result.put("specVersion", mBeanServerDelegateMBean.getSpecificationVersion());
       
   247         result.put("specVendor", mBeanServerDelegateMBean.getSpecificationVendor());
       
   248 
       
   249         result.put("implName", mBeanServerDelegateMBean.getImplementationName());
       
   250         result.put("implVersion", mBeanServerDelegateMBean.getImplementationVersion());
       
   251         result.put("implVendor", mBeanServerDelegateMBean.getImplementationVendor());
       
   252 
       
   253         return result;
       
   254     }
       
   255 
       
   256     private class AuthInvocationHandler implements InvocationHandler {
       
   257 
       
   258         private final MBeanServer mbeanServer;
       
   259         private final AccessControlContext acc;
       
   260 
       
   261         AuthInvocationHandler(MBeanServer server, Subject subject) {
       
   262             this.mbeanServer = server;
       
   263             if (subject == null) {
       
   264                 this.acc = null;
       
   265             } else {
       
   266                 if (SubjectDelegator.checkRemoveCallerContext(subject)) {
       
   267                     acc = JMXSubjectDomainCombiner.getDomainCombinerContext(subject);
       
   268                 } else {
       
   269                     acc = JMXSubjectDomainCombiner.getContext(subject);
       
   270                 }
       
   271             }
       
   272         }
       
   273 
       
   274         @Override
       
   275         public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
       
   276             if (acc == null) {
       
   277                 return method.invoke(mbeanServer, args);
       
   278             } else {
       
   279                 PrivilegedAction<Object> op = () -> {
       
   280                     try {
       
   281                         return method.invoke(mbeanServer, args);
       
   282                     } catch (Exception ex) {
       
   283                     }
       
   284                     return null;
       
   285                 };
       
   286                 return AccessController.doPrivileged(op, acc);
       
   287             }
       
   288         }
       
   289     }
       
   290 
       
   291     /*
       
   292     This is an expiring map that removes entries after the configured time period lapses.
       
   293     This is required to re-authenticate the user after the timeout.
       
   294      */
       
   295     private class TimedMap<K,V> {
       
   296 
       
   297         private ConcurrentHashMap<K,TimeStampedValue<V>>  permanentMap;
       
   298         private long timeout = Long.MAX_VALUE;   // Timeout in seconds
       
   299 
       
   300         private class TimeStampedValue<T> {
       
   301             private final T value;
       
   302             private final long insertTimeStamp;
       
   303 
       
   304             TimeStampedValue(T value) {
       
   305                 this.value = value;
       
   306                 insertTimeStamp = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
       
   307             }
       
   308         }
       
   309 
       
   310         public TimedMap(int seconds) {
       
   311             this.timeout = seconds;
       
   312             permanentMap = new ConcurrentHashMap<>();
       
   313         }
       
   314 
       
   315         public boolean containsKey(K key) {
       
   316             return permanentMap.containsKey(key);
       
   317         }
       
   318 
       
   319         public V get(K key) {
       
   320             TimeStampedValue<V> vTimeStampedValue = permanentMap.get(key);
       
   321             long current = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
       
   322             if(current - vTimeStampedValue.insertTimeStamp > timeout) {
       
   323                 permanentMap.remove(key);
       
   324                 return null;
       
   325             } else {
       
   326                 return vTimeStampedValue.value;
       
   327             }
       
   328         }
       
   329 
       
   330         public void put(K key, V value) {
       
   331             permanentMap.put(key, new TimeStampedValue<>(value));
       
   332         }
       
   333     }
       
   334 }