/*
* Copyright 2014, The Sporting Exchange Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.betfair.cougar.core.impl.jmx;
import com.betfair.cougar.core.api.jmx.JMXHttpParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.sun.jdmk.comm.HtmlParser;
import org.apache.commons.lang.StringEscapeUtils;
import javax.management.*;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.logging.Level;
public class HtmlAdaptorParser implements HtmlParser, DynamicMBean {
private static final Logger LOGGER = LoggerFactory.getLogger(HtmlAdaptorParser.class);
private static final String ADMIN_QUERY = "/administration/";
private List<JMXHttpParser> parsers = new ArrayList<JMXHttpParser>();
private MBeanServer mbs;
public HtmlAdaptorParser(MBeanServer server) {
this.mbs = server;
}
@Override
public String parsePage(String initialPage) {
return initialPage;
}
void addCustomParser(JMXHttpParser parser) {
parsers.add(parser);
}
@Override
public String parseRequest(String request) {
try {
LOGGER.debug("Parsing JMX/HTTP request {}", request);
if(!isValid(request)) {
LOGGER.warn("XSS attempt detected for request: "+request);
return "<h1>Illegal request</h1>";
}
if (request.startsWith(ADMIN_QUERY)) {
int paramIndex = request.indexOf('?');
String adminPage;
if (paramIndex == -1) {
adminPage = request.substring(ADMIN_QUERY.length());
} else {
adminPage = request.substring(ADMIN_QUERY.length(), paramIndex);
}
Map<String, String> params = getURIParams(request);
if (adminPage.equals("")) {
StringBuilder sb = new StringBuilder();
for (JMXHttpParser p: parsers) {
sb.append("<a href='").append(p.getPath()).append("'>").append(p.getPath()).append("</a><br/>");
}
return sb.toString();
}
else if (adminPage.equals("batchquery.jsp")) {
String objectName = params.get("on");
String attributeName = params.get("an");
String separator = "~";
String time = params.get("t");
StringBuilder buf = new StringBuilder();
if (time != null) {
buf.append("Time");
buf.append(separator);
DateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
buf.append(fmt.format(new Date()));
buf.append(separator);
}
if (objectName != null) {
if ("System".equalsIgnoreCase(objectName)) {
buf.append("System Properties");
Properties p = System.getProperties();
for (Map.Entry me: p.entrySet()) {
if (attributeName == null
||"ALL".equals(attributeName)
|| attributeName.equals(me.getKey())) {
buf.append(separator);
buf.append(me.getKey());
buf.append(separator);
buf.append(me.getValue());
}
}
} else {
query(buf, objectName, attributeName, separator);
}
}
return buf.toString();
}
// Loop through the available JmxHttpParsers to see if one matches
for (JMXHttpParser p: parsers) {
if (adminPage.equals(p.getPath())) {
return p.process(params);
}
}
}
return null;
} catch (Exception e) {
LOGGER.debug("Unable to retrieve Bean information", e);
return null;
}
}
@Override
public Object getAttribute(String attribute) throws AttributeNotFoundException, MBeanException, ReflectionException {
return null;
}
@Override
public AttributeList getAttributes(String[] attributes) {
return null;
}
@Override
public MBeanInfo getMBeanInfo() {
return new MBeanInfo(getClass().getName(), "HTML JMX request parser", null, null, null, null);
}
@Override
public Object invoke(String actionName, Object[] params, String[] signature) throws MBeanException, ReflectionException {
if (actionName.equals("parseRequest")) {
return parseRequest((String) params[0]);
} else if (actionName.equals("parsePage")) {
return parsePage((String) params[0]);
}
return null;
}
@Override
public void setAttribute(Attribute attribute) throws AttributeNotFoundException, InvalidAttributeValueException, MBeanException, ReflectionException {
}
@Override
public AttributeList setAttributes(AttributeList attributes) {
return null;
}
private Map<String, String> getURIParams(String uri) throws Exception {
Map<String, String> params = new HashMap<String, String>();
int qmi = uri.indexOf('?');
if (qmi >= 0) {
decodeParams(uri.substring(qmi + 1), params);
}
return params;
}
private void decodeParams(String params, Map<String, String> p) throws Exception {
StringTokenizer st = new StringTokenizer(params, "&");
while (st.hasMoreTokens()) {
String e = st.nextToken();
int sep = e.indexOf('=');
if (sep >= 0) {
p.put(decodePercent(e.substring(0, sep)).trim(), decodePercent(e.substring(sep + 1)));
} else {
p.put(decodePercent(e).trim(), "true");
}
}
}
/**
* Decodes the percent encoding scheme. <br/>
* For example: "an+example%20string" -> "an example string"
* @param str a string
* @return altered string
*/
private String decodePercent(String str) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
switch (c) {
case '+':
sb.append(' ');
break;
case '%':
sb.append((char) Integer.parseInt(str.substring(i + 1, i + 3), 16));
i += 2;
break;
default:
sb.append(c);
break;
}
}
str = sb.toString();
if (str.contains("%")) {
str = decodePercent(str);
}
return str;
}
/**
* Validates request string for not containing malicious characters.
*
* @param request a request string
* @return true - if request string is valid, false - otherwise
*/
private boolean isValid(String request) {
String tmp = decodePercent(request);
if(tmp.indexOf('<') != -1 ||
tmp.indexOf('>') != -1 ) {
return false;
}
return true;
}
private void query(StringBuilder buf, String son, String attrName, String separator) {
try {
ObjectName on = new ObjectName(son);
if (on.isPattern()) {
Set<ObjectInstance> res = mbs.queryMBeans(on, null);
if (res != null && res.size() > 0) {
Iterator<ObjectInstance> j = res.iterator();
while (j.hasNext()) {
on = j.next().getObjectName();
appendMBean(mbs, on, attrName, separator, buf);
if (j.hasNext()) {
buf.append("|");
}
}
}
} else {
appendMBean(mbs, on, attrName, separator, buf);
}
} catch (Exception e) {
LOGGER.debug("Unable to retrieve Bean information for bean "+son, e);
}
}
private void runOperation(StringBuilder buf, String son, String operation, String separator) {
try {
ObjectName on = new ObjectName(son);
Object result = mbs.invoke(on, operation, new Object[0], new String[0]);
buf.append(on.toString())
.append(separator)
.append(operation)
.append(separator)
.append(result)
.append(separator);
} catch (Exception e) {
LOGGER.debug("Unable to run operation {} on bean {} ", operation, separator);
}
}
private void appendMBean(MBeanServer server, ObjectName on, String attrName, String separator, StringBuilder buf) {
StringBuilder local = new StringBuilder();
try {
MBeanInfo info = server.getMBeanInfo(on);
local.append(on.toString());
MBeanAttributeInfo[] attr = info.getAttributes();
for (int i = 0; i < attr.length; i++) {
if ((attrName == null || attrName.equals(attr[i].getName())) && attr[i].isReadable()) {
local.append(separator);
local.append(attr[i].getName());
local.append(separator);
local.append(server.getAttribute(on, attr[i].getName()));
}
}
} catch (Exception e) {
LOGGER.debug("Unable to retrieve Bean information for bean "+on, e);
return;
}
buf.append(local);
}
}