1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.hadoop.hbase.http.jmx;
19
20 import java.io.IOException;
21 import java.io.PrintWriter;
22 import java.lang.management.ManagementFactory;
23
24 import javax.management.MBeanServer;
25 import javax.management.MalformedObjectNameException;
26 import javax.management.ObjectName;
27 import javax.management.ReflectionException;
28 import javax.management.RuntimeErrorException;
29 import javax.management.RuntimeMBeanException;
30 import javax.management.openmbean.CompositeData;
31 import javax.management.openmbean.TabularData;
32 import javax.servlet.ServletException;
33 import javax.servlet.http.HttpServlet;
34 import javax.servlet.http.HttpServletRequest;
35 import javax.servlet.http.HttpServletResponse;
36
37 import org.apache.commons.logging.Log;
38 import org.apache.commons.logging.LogFactory;
39 import org.apache.hadoop.hbase.http.HttpServer;
40 import org.apache.hadoop.hbase.util.JSONBean;
41 import org.owasp.esapi.ESAPI;
42
43 /*
44 * This servlet is based off of the JMXProxyServlet from Tomcat 7.0.14. It has
45 * been rewritten to be read only and to output in a JSON format so it is not
46 * really that close to the original.
47 */
48 /**
49 * Provides Read only web access to JMX.
50 * <p>
51 * This servlet generally will be placed under the /jmx URL for each
52 * HttpServer. It provides read only
53 * access to JMX metrics. The optional <code>qry</code> parameter
54 * may be used to query only a subset of the JMX Beans. This query
55 * functionality is provided through the
56 * {@link MBeanServer#queryNames(ObjectName, javax.management.QueryExp)}
57 * method.
58 * </p>
59 * <p>
60 * For example <code>http://.../jmx?qry=Hadoop:*</code> will return
61 * all hadoop metrics exposed through JMX.
62 * </p>
63 * <p>
64 * The optional <code>get</code> parameter is used to query an specific
65 * attribute of a JMX bean. The format of the URL is
66 * <code>http://.../jmx?get=MXBeanName::AttributeName</code>
67 * </p>
68 * <p>
69 * For example
70 * <code>
71 * http://../jmx?get=Hadoop:service=NameNode,name=NameNodeInfo::ClusterId
72 * </code> will return the cluster id of the namenode mxbean.
73 * </p>
74 * <p>
75 * If the <code>qry</code> or the <code>get</code> parameter is not formatted
76 * correctly then a 400 BAD REQUEST http response code will be returned.
77 * </p>
78 * <p>
79 * If a resouce such as a mbean or attribute can not be found,
80 * a 404 SC_NOT_FOUND http response code will be returned.
81 * </p>
82 * <p>
83 * The return format is JSON and in the form
84 * </p>
85 * <pre><code>
86 * {
87 * "beans" : [
88 * {
89 * "name":"bean-name"
90 * ...
91 * }
92 * ]
93 * }
94 * </code></pre>
95 * <p>
96 * The servlet attempts to convert the the JMXBeans into JSON. Each
97 * bean's attributes will be converted to a JSON object member.
98 *
99 * If the attribute is a boolean, a number, a string, or an array
100 * it will be converted to the JSON equivalent.
101 *
102 * If the value is a {@link CompositeData} then it will be converted
103 * to a JSON object with the keys as the name of the JSON member and
104 * the value is converted following these same rules.
105 *
106 * If the value is a {@link TabularData} then it will be converted
107 * to an array of the {@link CompositeData} elements that it contains.
108 *
109 * All other objects will be converted to a string and output as such.
110 *
111 * The bean's name and modelerType will be returned for all beans.
112 *
113 * Optional paramater "callback" should be used to deliver JSONP response.
114 * </p>
115 *
116 */
117 public class JMXJsonServlet extends HttpServlet {
118 private static final Log LOG = LogFactory.getLog(JMXJsonServlet.class);
119
120 private static final long serialVersionUID = 1L;
121
122 private static final String CALLBACK_PARAM = "callback";
123 /**
124 * If query string includes 'description', then we will emit bean and attribute descriptions to
125 * output IFF they are not null and IFF the description is not the same as the attribute name:
126 * i.e. specify an URL like so: /jmx?description=true
127 */
128 private static final String INCLUDE_DESCRIPTION = "description";
129
130 /**
131 * MBean server.
132 */
133 protected transient MBeanServer mBeanServer;
134
135 protected transient JSONBean jsonBeanWriter;
136
137 /**
138 * Initialize this servlet.
139 */
140 @Override
141 public void init() throws ServletException {
142 // Retrieve the MBean server
143 mBeanServer = ManagementFactory.getPlatformMBeanServer();
144 this.jsonBeanWriter = new JSONBean();
145 }
146
147 /**
148 * Process a GET request for the specified resource.
149 *
150 * @param request
151 * The servlet request we are processing
152 * @param response
153 * The servlet response we are creating
154 */
155 @Override
156 @edu.umd.cs.findbugs.annotations.SuppressWarnings(value="XSS_REQUEST_PARAMETER_TO_SERVLET_WRITER",
157 justification="TODO: See HBASE-15122")
158 public void doGet(HttpServletRequest request, HttpServletResponse response) {
159 try {
160 if (!HttpServer.isInstrumentationAccessAllowed(getServletContext(), request, response)) {
161 return;
162 }
163 String jsonpcb = null;
164 PrintWriter writer = null;
165 JSONBean.Writer beanWriter = null;
166 try {
167 writer = response.getWriter();
168 beanWriter = this.jsonBeanWriter.open(writer);
169 // "callback" parameter implies JSONP outpout
170 jsonpcb = request.getParameter(CALLBACK_PARAM);
171 if (jsonpcb != null) {
172 response.setContentType("application/javascript; charset=utf8");
173 writer.write(encodeJS(jsonpcb) + "(");
174 } else {
175 response.setContentType("application/json; charset=utf8");
176 }
177 // Should we output description on each attribute and bean?
178 String tmpStr = request.getParameter(INCLUDE_DESCRIPTION);
179 boolean description = tmpStr != null && tmpStr.length() > 0;
180
181 // query per mbean attribute
182 String getmethod = request.getParameter("get");
183 if (getmethod != null) {
184 String[] splitStrings = getmethod.split("\\:\\:");
185 if (splitStrings.length != 2) {
186 beanWriter.write("result", "ERROR");
187 beanWriter.write("message", "query format is not as expected.");
188 beanWriter.flush();
189 response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
190 return;
191 }
192 if (beanWriter.write(this.mBeanServer, new ObjectName(splitStrings[0]),
193 splitStrings[1], description) != 0) {
194 beanWriter.flush();
195 response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
196 }
197 return;
198 }
199
200 // query per mbean
201 String qry = request.getParameter("qry");
202 if (qry == null) {
203 qry = "*:*";
204 }
205 if (beanWriter.write(this.mBeanServer, new ObjectName(qry), null, description) != 0) {
206 beanWriter.flush();
207 response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
208 }
209 } finally {
210 if (beanWriter != null) beanWriter.close();
211 if (jsonpcb != null) {
212 writer.write(");");
213 }
214 if (writer != null) {
215 writer.close();
216 }
217 }
218 } catch (IOException e) {
219 LOG.error("Caught an exception while processing JMX request", e);
220 response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
221 } catch (MalformedObjectNameException e) {
222 LOG.error("Caught an exception while processing JMX request", e);
223 response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
224 }
225 }
226
227 private String encodeJS(String inputStr) {
228 return ESAPI.encoder().encodeForJavaScript(inputStr);
229 }
230
231 }