001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.hadoop.hbase.http.jmx;
019
020import java.io.IOException;
021import java.io.PrintWriter;
022import java.lang.management.ManagementFactory;
023import javax.management.MBeanServer;
024import javax.management.MalformedObjectNameException;
025import javax.management.ObjectName;
026import javax.management.openmbean.CompositeData;
027import javax.management.openmbean.TabularData;
028import javax.servlet.ServletException;
029import javax.servlet.http.HttpServlet;
030import javax.servlet.http.HttpServletRequest;
031import javax.servlet.http.HttpServletResponse;
032import org.apache.hadoop.hbase.http.HttpServer;
033import org.apache.hadoop.hbase.util.JSONBean;
034import org.apache.yetus.audience.InterfaceAudience;
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038/*
039 * This servlet is based off of the JMXProxyServlet from Tomcat 7.0.14. It has
040 * been rewritten to be read only and to output in a JSON format so it is not
041 * really that close to the original.
042 */
043/**
044 * Provides Read only web access to JMX.
045 * <p>
046 * This servlet generally will be placed under the /jmx URL for each
047 * HttpServer.  It provides read only
048 * access to JMX metrics.  The optional <code>qry</code> parameter
049 * may be used to query only a subset of the JMX Beans.  This query
050 * functionality is provided through the
051 * {@link MBeanServer#queryNames(ObjectName, javax.management.QueryExp)}
052 * method.
053 * </p>
054 * <p>
055 * For example <code>http://.../jmx?qry=Hadoop:*</code> will return
056 * all hadoop metrics exposed through JMX.
057 * </p>
058 * <p>
059 * The optional <code>get</code> parameter is used to query an specific
060 * attribute of a JMX bean.  The format of the URL is
061 * <code>http://.../jmx?get=MXBeanName::AttributeName</code>
062 * </p>
063 * <p>
064 * For example
065 * <code>
066 * http://../jmx?get=Hadoop:service=NameNode,name=NameNodeInfo::ClusterId
067 * </code> will return the cluster id of the namenode mxbean.
068 * </p>
069 * <p>
070 * If the <code>qry</code> or the <code>get</code> parameter is not formatted
071 * correctly then a 400 BAD REQUEST http response code will be returned.
072 * </p>
073 * <p>
074 * If a resouce such as a mbean or attribute can not be found,
075 * a 404 SC_NOT_FOUND http response code will be returned.
076 * </p>
077 * <p>
078 * The return format is JSON and in the form
079 * </p>
080 *  <pre><code>
081 *  {
082 *    "beans" : [
083 *      {
084 *        "name":"bean-name"
085 *        ...
086 *      }
087 *    ]
088 *  }
089 *  </code></pre>
090 *  <p>
091 *  The servlet attempts to convert the the JMXBeans into JSON. Each
092 *  bean's attributes will be converted to a JSON object member.
093 *
094 *  If the attribute is a boolean, a number, a string, or an array
095 *  it will be converted to the JSON equivalent.
096 *
097 *  If the value is a {@link CompositeData} then it will be converted
098 *  to a JSON object with the keys as the name of the JSON member and
099 *  the value is converted following these same rules.
100 *
101 *  If the value is a {@link TabularData} then it will be converted
102 *  to an array of the {@link CompositeData} elements that it contains.
103 *
104 *  All other objects will be converted to a string and output as such.
105 *
106 *  The bean's name and modelerType will be returned for all beans.
107 *
108 *  Optional paramater "callback" should be used to deliver JSONP response.
109 * </p>
110 *
111 */
112@InterfaceAudience.Private
113public class JMXJsonServlet extends HttpServlet {
114  private static final Logger LOG = LoggerFactory.getLogger(JMXJsonServlet.class);
115
116  private static final long serialVersionUID = 1L;
117
118  private static final String CALLBACK_PARAM = "callback";
119  /**
120   * If query string includes 'description', then we will emit bean and attribute descriptions to
121   * output IFF they are not null and IFF the description is not the same as the attribute name:
122   * i.e. specify an URL like so: /jmx?description=true
123   */
124  private static final String INCLUDE_DESCRIPTION = "description";
125
126  /**
127   * MBean server.
128   */
129  protected transient MBeanServer mBeanServer;
130
131  protected transient JSONBean jsonBeanWriter;
132
133  /**
134   * Initialize this servlet.
135   */
136  @Override
137  public void init() throws ServletException {
138    // Retrieve the MBean server
139    mBeanServer = ManagementFactory.getPlatformMBeanServer();
140    this.jsonBeanWriter = new JSONBean();
141  }
142
143  /**
144   * Process a GET request for the specified resource.
145   *
146   * @param request
147   *          The servlet request we are processing
148   * @param response
149   *          The servlet response we are creating
150   */
151  @Override
152  public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
153    try {
154      if (!HttpServer.isInstrumentationAccessAllowed(getServletContext(), request, response)) {
155        return;
156      }
157      String jsonpcb = null;
158      PrintWriter writer = null;
159      JSONBean.Writer beanWriter = null;
160      try {
161        jsonpcb = checkCallbackName(request.getParameter(CALLBACK_PARAM));
162        writer = response.getWriter();
163        beanWriter = this.jsonBeanWriter.open(writer);
164
165        // "callback" parameter implies JSONP outpout
166        if (jsonpcb != null) {
167          response.setContentType("application/javascript; charset=utf8");
168          writer.write(jsonpcb + "(");
169        } else {
170          response.setContentType("application/json; charset=utf8");
171        }
172        // Should we output description on each attribute and bean?
173        String tmpStr = request.getParameter(INCLUDE_DESCRIPTION);
174        boolean description = tmpStr != null && tmpStr.length() > 0;
175
176        // query per mbean attribute
177        String getmethod = request.getParameter("get");
178        if (getmethod != null) {
179          String[] splitStrings = getmethod.split("\\:\\:");
180          if (splitStrings.length != 2) {
181            beanWriter.write("result", "ERROR");
182            beanWriter.write("message", "query format is not as expected.");
183            beanWriter.flush();
184            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
185            return;
186          }
187          if (beanWriter.write(this.mBeanServer, new ObjectName(splitStrings[0]),
188              splitStrings[1], description) != 0) {
189            beanWriter.flush();
190            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
191          }
192          return;
193        }
194
195        // query per mbean
196        String qry = request.getParameter("qry");
197        if (qry == null) {
198          qry = "*:*";
199        }
200        if (beanWriter.write(this.mBeanServer, new ObjectName(qry), null, description) != 0) {
201          beanWriter.flush();
202          response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
203        }
204      } finally {
205        if (beanWriter != null) beanWriter.close();
206        if (jsonpcb != null) {
207           writer.write(");");
208        }
209        if (writer != null) {
210          writer.close();
211        }
212      }
213    } catch (IOException e) {
214      LOG.error("Caught an exception while processing JMX request", e);
215      response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
216    } catch (MalformedObjectNameException e) {
217      LOG.error("Caught an exception while processing JMX request", e);
218      response.sendError(HttpServletResponse.SC_BAD_REQUEST);
219    }
220  }
221
222  /**
223   * Verifies that the callback property, if provided, is purely alphanumeric.
224   * This prevents a malicious callback name (that is javascript code) from being
225   * returned by the UI to an unsuspecting user.
226   *
227   * @param callbackName The callback name, can be null.
228   * @return The callback name
229   * @throws IOException If the name is disallowed.
230   */
231  private String checkCallbackName(String callbackName) throws IOException {
232    if (null == callbackName) {
233      return null;
234    }
235    if (callbackName.matches("[A-Za-z0-9_]+")) {
236      return callbackName;
237    }
238    throw new IOException("'callback' must be alphanumeric");
239  }
240}