blob: bf093ac9841f8279670570776a7307240cb6f098 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.felix.mosgi.jmx.httpconnector.mx4j.tools.adaptor.http;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;
import javax.management.JMException;
import javax.management.MBeanRegistration;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.osgi.framework.ServiceReference;
import org.osgi.service.log.LogService;
import org.apache.felix.mosgi.jmx.httpconnector.mx4j.tools.adaptor.AdaptorServerSocketFactory;
import org.apache.felix.mosgi.jmx.httpconnector.mx4j.tools.adaptor.PlainAdaptorServerSocketFactory;
import org.apache.felix.mosgi.jmx.agent.mx4j.util.Base64Codec;
import org.apache.felix.mosgi.jmx.httpconnector.HttpConnectorActivator;
import org.apache.felix.mosgi.jmx.httpconnector.mx4j.tools.adaptor.http.ServerCommandProcessor;
import org.apache.felix.mosgi.jmx.httpconnector.mx4j.tools.adaptor.http.ServerByDomainCommandProcessor;
import org.apache.felix.mosgi.jmx.httpconnector.mx4j.tools.adaptor.http.MBeanCommandProcessor;
import org.apache.felix.mosgi.jmx.httpconnector.mx4j.tools.adaptor.http.SetAttributesCommandProcessor;
import org.apache.felix.mosgi.jmx.httpconnector.mx4j.tools.adaptor.http.DeleteMBeanCommandProcessor;
import org.apache.felix.mosgi.jmx.httpconnector.mx4j.tools.adaptor.http.InvokeOperationCommandProcessor;
import org.apache.felix.mosgi.jmx.httpconnector.mx4j.tools.adaptor.http.CreateMBeanCommandProcessor;
import org.apache.felix.mosgi.jmx.httpconnector.mx4j.tools.adaptor.http.ConstructorsCommandProcessor;
import org.apache.felix.mosgi.jmx.httpconnector.mx4j.tools.adaptor.http.EmptyCommandProcessor;
/**
* HttpAdaptor sets the basic adaptor listening for HTTP requests
*
* @author <a href="mailto:tibu@users.sourceforge.net">Carlos Quiroz</a>
* @version $Revision: 1.1.1.1 $
*/
public class HttpAdaptor implements HttpAdaptorMBean, MBeanRegistration
{
private static final String VERSION = "2.0 Beta 1";
/** Port to listen for connections */
private int port = 8080;
/** Host where to set the server socket */
private String host = "localhost";
/** Target server */
private MBeanServer server;
/** Server socket */
private ServerSocket serverSocket;
/** Indicates whether the server is running */
private boolean alive;
/** Map of commands indexed by the request path */
private Map commands = new HashMap();
/** Target processor */
private ProcessorMBean processor = null;
/** Target processor name */
private ObjectName processorName = null;
/** Default processor */
private ProcessorMBean defaultProcessor = new DefaultProcessor();
private String authenticationMethod = "none";
// Should be dependant on the server?
private String realm="MX4J";
private Map authorizations = new HashMap();
private AdaptorServerSocketFactory socketFactory = null;
private ObjectName factoryName;
private String processorClass;
private Date startDate;
private long requestsCount;
private String[][] defaultCommandProcessors = {
{"server", ServerCommandProcessor.class.getName()},
{"serverbydomain", ServerByDomainCommandProcessor.class.getName()},
{"mbean", MBeanCommandProcessor.class.getName()},
{"setattributes", SetAttributesCommandProcessor.class.getName()},
{"setattribute", SetAttributeCommandProcessor.class.getName()},
{"getattribute", GetAttributeCommandProcessor.class.getName()},
{"delete", DeleteMBeanCommandProcessor.class.getName()},
{"invoke", InvokeOperationCommandProcessor.class.getName()},
{"create", CreateMBeanCommandProcessor.class.getName()},
{"constructors", ConstructorsCommandProcessor.class.getName()},
{"empty", EmptyCommandProcessor.class.getName()}};
// {"relation", "mx4j.tools.adaptor.http.RelationCommandProcessor"},
private DocumentBuilder builder;
/**
* Default Constructor added so that we can have some additional
* constructors as well.
*/
public HttpAdaptor()
{
}
/**
* Overloaded constructor to allow the port to be set.
* The reason this was added was to allow the loading of this adaptor by
* the dynamic loading service of the MBean server and have the port set
* from a param in the mlet file. Example: (replaced lt & gt symbol with [])
* <br>[mlet code="mx4j.tools.adaptor.http.HttpAdaptor"
* <br> archive="mx4j.jar"
* <br> name="Server:name=HttpAdaptor"]
* <br> [arg type="int" value="12345"]
* <br>[/mlet]
*
* <p>This constructor uses the default host or the host must be set later.
* @param port The port on which the HttpAdaptor should listen
*/
public HttpAdaptor(int port)
{
this.port = port;
}
/**
* Overloaded constructor to allow the host to be set.
* The reason this was added was to allow the loading of this adaptor by
* the dynamic loading service of the MBean server and have the host set
* from a param in the mlet file. Example: (replaced lt & gt symbol with [])
* <br>[mlet code="mx4j.tools.adaptor.http.HttpAdaptor"
* <br> archive="mx4j.jar"
* <br> name="Server:name=HttpAdaptor"]
* <br> [arg type="java.lang.String" value="someserver.somehost.com"]
* <br>[/mlet]
*
* <p>This constructor uses the default port or the port must be set later.
* @param host The host on which the HttpAdaptor should listen
*/
public HttpAdaptor(String host)
{
this.host = host;
}
/**
* Overloaded constructor to allow the port to be set.
* The reason this was added was to allow the loading of this adaptor by
* the dynamic loading service of the MBean server and have the port set
* from a param in the mlet file. Example: (replaced lt & gt symbol with [])
* NOTE that the port must come before the host in the arg list of the mlet
* <br>[mlet code="mx4j.tools.adaptor.http.HttpAdaptor"
* <br> archive="mx4j.jar"
* <br> name="Server:name=HttpAdaptor"]
* <br> [arg type="int" value="12345"]
* <br> [arg type="java.lang.String" value="someserver.somehost.com"]
* <br>[/mlet]
*
* @param port The port on which the HttpAdaptor should listen
* @param host The host on which the HttpAdaptor should listen
*/
public HttpAdaptor(int port, String host)
{
this.port = port;
this.host = host;
}
/**
* Sets the value of the server's port
*
* @param port the new port's value
*/
public void setPort(int port)
{
if (alive)
{
throw new IllegalArgumentException("Not possible to change port with the server running");
}
this.port = port;
}
/**
* Returns the port where the server is running on. Default is 8080
*
* @return HTTPServer's port
*/
public int getPort()
{
return port;
}
/**
* Sets the host name where the server will be listening
*
* @param host Server's host
*/
public void setHost(String host)
{
if (alive)
{
throw new IllegalArgumentException("Not possible to change port with the server running");
}
this.host = host;
}
/**
* Return the host name the server will be listening to. If null the server
* listen at the localhost
*
* @return the current hostname
*/
public String getHost()
{
return host;
}
/**
* Sets the Authentication Method.
*
* @param method none/basic/digest
*/
public void setAuthenticationMethod(String method) {
if (alive)
{
throw new IllegalArgumentException("Not possible to change authentication method with the server running");
}
if (method == null || !(method.equals("none") || method.equals("basic") || method.equals("digest")))
{
throw new IllegalArgumentException("Only accept methods none/basic/digest");
}
this.authenticationMethod = method;
}
/**
* Authentication Method
*
* @return authentication method
*/
public String getAuthenticationMethod()
{
return authenticationMethod;
}
/**
* Sets the object which will post process the XML results. The last value set
* between the setPostProcessor and setPostProcessorName will be the valid one
*
* @param processor a Post processor object
*/
public void setProcessor(ProcessorMBean processor)
{
this.processor = processor;
this.processorName = null;
}
/**
* Sets the classname of the object which will post process the XML results. The adaptor
* will try to build the object and use the processor name ObjectName to register it
* The class name has to implements mx4j.tools.adaptor.http.ProcessorMBean and be MBean
* compliant
* @param processorClass a Post processor object
*/
public void setProcessorClass(String processorClass)
{
this.processorClass = processorClass;
}
/**
* Sets the object name of the PostProcessor MBean. If ProcessorClass is set the processor
* will be created
* @param processorName a Post processor object
*/
public void setProcessorNameString(String processorName) throws MalformedObjectNameException
{
this.processorName = new ObjectName(processorName);
}
/**
* Sets the object name which will post process the XML result. The last value
* set between the setPostProcessor and setPostProcessorName will be the valid
* one. The MBean will be verified to be of instance HttpPostProcessor
* @param processorName The new processorName value
*/
public void setProcessorName(ObjectName processorName)
{
this.processor = null;
this.processorName = processorName;
}
public ProcessorMBean getProcessor()
{
return this.processor;
}
public ObjectName getProcessorName()
{
return this.processorName;
}
/**
* Sets the object which create the server sockets
*
* @param factory the socket factory
*/
public void setSocketFactory(AdaptorServerSocketFactory factory)
{
this.factoryName = null;
this.socketFactory = factory;
}
/**
* Sets the factory's object name which will create the server sockets
*
* @param factoryName the socket factory
*/
public void setSocketFactoryName(ObjectName factoryName)
{
this.socketFactory = null;
this.factoryName = factoryName;
}
/**
* Sets the factory's object name which will create the server sockets
*
* @param factoryName the socket factory
*/
public void setSocketFactoryNameString(String factoryName) throws MalformedObjectNameException
{
this.socketFactory = null;
this.factoryName = new ObjectName(factoryName);
}
/**
* Indicates whether the server's running
*
* @return The active value
*/
public boolean isActive()
{
return alive;
}
/**
* Starting date
*
* @return The date when the server was started
*/
public Date getStartDate()
{
return startDate;
}
/**
* Requests count
*
* @return The total of requests served so far
*/
public long getRequestsCount()
{
return requestsCount;
}
/**
* Gets the HttpAdaptor version
*
* @return HttpAdaptor's version
*/
public String getVersion()
{
return VERSION;
}
/**
* Adds a command processor object
*/
public void addCommandProcessor(String path, HttpCommandProcessor processor)
{
commands.put(path, processor);
if (alive)
{
processor.setMBeanServer(server);
processor.setDocumentBuilder(builder);
}
}
/**
* Adds a command processor object by class
*/
public void addCommandProcessor(String path, String processorClass)
{
try
{
HttpCommandProcessor processor = (HttpCommandProcessor)Class.forName(processorClass).newInstance();
addCommandProcessor(path, processor);
}
catch (Exception e)
{
HttpAdaptor.log(LogService.LOG_ERROR,
"Exception creating Command Processor of class " + processorClass,
e);
}
}
/**
* Removes a command processor object by class
*/
public void removeCommandProcessor(String path)
{
if (commands.containsKey(path))
{
commands.remove(path);
}
}
/**
* Starts the server
*/
public void start()
throws IOException
{
if (server != null)
{
serverSocket = createServerSocket();
if (serverSocket == null)
{
HttpAdaptor.log(LogService.LOG_ERROR, "Server socket is null", null);
return;
}
if (processorClass != null && processorName != null)
{
HttpAdaptor.log(LogService.LOG_INFO,"Building processor class of type " + processorClass + " and name " + processorName, null);
try {
server.createMBean(processorClass, processorName, null);
} catch (JMException e) {
HttpAdaptor.log(LogService.LOG_INFO, "Exception creating processor class", e);
}
}
Iterator i = commands.values().iterator();
while (i.hasNext())
{
HttpCommandProcessor processor = (HttpCommandProcessor)i.next();
processor.setMBeanServer(server);
processor.setDocumentBuilder(builder);
}
HttpAdaptor.log(LogService.LOG_INFO, "HttpAdaptor server listening on port " + port, null);
alive = true;
Thread serverThread = new Thread(
new Runnable() {
public void run() {
HttpAdaptor.log(LogService.LOG_INFO, "HttpAdaptor version " + VERSION + " started",null);
startDate = new Date();
requestsCount = 0;
while (alive)
{
try
{
Socket client = null;
client = serverSocket.accept();
if (!alive)
{
break;
}
requestsCount++;
new HttpClient(client).start();
}
catch (InterruptedIOException e)
{
continue;
}
catch (IOException e)
{
continue;
}
catch (Exception e)
{
HttpAdaptor.log(LogService.LOG_WARNING, "Exception during request processing", e);
continue;
}
catch (Error e)
{
HttpAdaptor.log(LogService.LOG_ERROR, "Error during request processing", e);
continue;
}
}
try {
serverSocket.close();
} catch (IOException e) {
HttpAdaptor.log(LogService.LOG_WARNING, "Exception closing the server", e);
}
serverSocket = null;
alive = false;
HttpAdaptor.log(LogService.LOG_INFO, "Server stopped", null);
}
});
serverThread.start();
}
else
{
HttpAdaptor.log(LogService.LOG_INFO,"Start failed, no server target server has been set",null);
}
}
/**
* Restarts the server. Useful when changing the Server parameters
*
* @deprecated as of RC 1
*/
public void restart()
throws IOException
{
stop();
start();
}
/**
* Stops the HTTP daemon
*/
public void stop()
{
try
{
if (alive)
{
alive = false;
// force the close with a socket call
new Socket(host, port);
}
}
catch (IOException e)
{
HttpAdaptor.log(LogService.LOG_WARNING,e.getMessage(),e);
}
try {
if (serverSocket != null)
{
serverSocket.close();
}
}
catch (IOException e)
{
HttpAdaptor.log(LogService.LOG_WARNING,e.getMessage(),e);
}
}
/**
* Adds an authorization pair as username/password
*/
public void addAuthorization(String username, String password)
{
if (username == null || password == null)
{
throw new IllegalArgumentException("username and passwords cannot be null");
}
authorizations.put(username, password);
}
/**
* Gathers some basic data
*/
public ObjectName preRegister(MBeanServer server, ObjectName name)
throws java.lang.Exception
{
this.server = server;
buildCommands();
return name;
}
public void postRegister(Boolean registrationDone) { }
public void preDeregister()
throws java.lang.Exception
{
// stop the server
stop();
}
public void postDeregister() { }
private ServerSocket createServerSocket() throws IOException
{
if (socketFactory == null)
{
if (factoryName == null)
{
socketFactory = new PlainAdaptorServerSocketFactory();
return socketFactory.createServerSocket(port, 50, host);
}
else
{
try
{
return (ServerSocket)server.invoke(factoryName, "createServerSocket", new Object[] {new Integer(port), new Integer(50), host}, new String[] {"int", "int", "java.lang.String"});
}
catch (Exception x)
{
HttpAdaptor.log(LogService.LOG_ERROR,"Exception invoking AdaptorServerSocketFactory via MBeanServer", x);
}
}
}
else
{
return socketFactory.createServerSocket(port, 50, host);
}
return null;
}
private boolean isUsernameValid(String username, String password)
{
if (authorizations.containsKey(username))
{
return password.equals(authorizations.get(username));
}
return false;
}
protected HttpCommandProcessor getProcessor(String path)
{
return (HttpCommandProcessor)commands.get(path);
}
/**
* Build the commands
*/
protected void buildCommands()
{
try
{
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
builder = factory.newDocumentBuilder();
for (int i=0;i<defaultCommandProcessors.length;i++)
{
try
{
HttpCommandProcessor processor = (HttpCommandProcessor)Class.forName(defaultCommandProcessors[i][1]).newInstance();
commands.put(defaultCommandProcessors[i][0], processor);
} catch (Exception e) {
HttpAdaptor.log(LogService.LOG_WARNING,"Exception building command procesor", e);
}
}
}
catch (ParserConfigurationException e)
{
HttpAdaptor.log(LogService.LOG_ERROR,"Exception building the Document Factories", e);
}
}
protected void postProcess(HttpOutputStream out, HttpInputStream in, Document document)
throws IOException, JMException
{
boolean processed = false;
// inefficient but handles modifications at runtime
if (processorName != null)
{
if (server.isRegistered(processorName) &&
server.isInstanceOf(processorName, ProcessorMBean.class.getName()))
{
server.invoke(processorName,
"writeResponse",
new Object[]{out, in, document},
new String[]{HttpOutputStream.class.getName(), HttpInputStream.class.getName(), Document.class.getName()});
processed = true;
}
else
{
HttpAdaptor.log(LogService.LOG_DEBUG,processorName + " not found",null);
}
}
if (!processed && processor != null)
{
processor.writeResponse(out, in, document);
processed = true;
}
if (!processed)
{
defaultProcessor.writeResponse(out, in, document);
}
}
protected void findUnknownElement(String path, HttpOutputStream out, HttpInputStream in)
throws IOException, JMException
{
boolean processed = false;
// inefficient but handles modifications at runtime
if (processorName != null)
{
if (server.isRegistered(processorName) &&
server.isInstanceOf(processorName, ProcessorMBean.class.getName()))
{
server.invoke(processorName,
"notFoundElement",
new Object[]{path, out, in},
new String[]{String.class.getName(), HttpOutputStream.class.getName(), HttpInputStream.class.getName()});
processed = true;
}
else
{
HttpAdaptor.log(LogService.LOG_DEBUG,processorName + " not found",null);
}
}
if (!processed && processor != null)
{
processor.notFoundElement(path, out, in);
processed = true;
}
if (!processed)
{
defaultProcessor.notFoundElement(path, out, in);
}
}
protected String preProcess(String path)
throws IOException, JMException
{
boolean processed = false;
// inefficient but handles modifications at runtime
if (processorName != null)
{
HttpAdaptor. log(LogService.LOG_DEBUG,"Preprocess using " + processorName,null);
if (server.isRegistered(processorName) &&
server.isInstanceOf(processorName, ProcessorMBean.class.getName()))
{
HttpAdaptor.log(LogService.LOG_DEBUG,"Preprocessing",null);
path = (String)server.invoke(processorName,
"preProcess",
new Object[]{path},
new String[]{String.class.getName()});
processed = true;
}
else
{
HttpAdaptor.log(LogService.LOG_DEBUG,processorName + " not found",null);
}
}
if (!processed && processor != null)
{
path = processor.preProcess(path);
processed = true;
}
if (!processed)
{
path = defaultProcessor.preProcess(path);
}
return path;
}
protected void postProcess(HttpOutputStream out, HttpInputStream in, Exception e)
throws IOException, JMException
{
boolean processed = false;
// inefficient but handles modifications at runtime
if (processorName != null)
{
if (server.isRegistered(processorName) &&
server.isInstanceOf(processorName, ProcessorMBean.class.getName()))
{
server.invoke(processorName,
"writeError",
new Object[]{out, in, e},
new String[]{HttpOutputStream.class.getName(),HttpInputStream.class.getName(), Exception.class.getName()});
processed = true;
}
else
{
HttpAdaptor.log(LogService.LOG_DEBUG,processorName + " not found",null);
}
}
if (!processed && processor != null)
{
processor.writeError(out, in, e);
processed = true;
}
if (!processed)
{
defaultProcessor.writeError(out, in, e);
}
}
private class HttpClient extends Thread
{
private Socket client;
HttpClient(Socket client)
{
this.client = client;
}
public boolean isValid(String authorizationString) {
if (authenticationMethod.startsWith("basic"))
{
authorizationString = authorizationString.substring(5,authorizationString.length());
String decodeString = new String(Base64Codec.decodeBase64(authorizationString.getBytes()));
if (decodeString.indexOf(":")>0)
{
try
{
StringTokenizer tokens = new StringTokenizer(decodeString, ":");
String username = tokens.nextToken();
String password = tokens.nextToken();
return isUsernameValid(username, password);
}
catch (Exception e)
{
return false;
}
}
}
return false;
}
private boolean handleAuthentication(HttpInputStream in, HttpOutputStream out) throws IOException {
if (authenticationMethod.equals("basic"))
{
String result = in.getHeader("authorization");
if (result != null)
{
if (isValid(result))
{
return true;
}
throw new HttpException(HttpConstants.STATUS_FORBIDDEN, "Authentication failed");
}
out.setCode(HttpConstants.STATUS_AUTHENTICATE);
out.setHeader("WWW-Authenticate", "Basic realm=\"" + realm + "\"");
out.sendHeaders();
out.flush();
return false;
}
if (authenticationMethod.equals("digest"))
{
// not implemented
}
return true;
}
public void run()
{
HttpInputStream httpIn = null;
HttpOutputStream httpOut = null;
try
{
// get input streams
InputStream in = client.getInputStream();
httpIn = new HttpInputStream(in);
httpIn.readRequest();
// Find a suitable command processor
String path = httpIn.getPath();
String queryString = httpIn.getQueryString();
HttpAdaptor.log(LogService.LOG_INFO,"Request " + path + ((queryString == null) ? "" : ("?" + queryString)), null);
String postPath = preProcess(path);
if (!postPath.equals(path))
{
HttpAdaptor.log(LogService.LOG_INFO,"Processor replaced path " + path + " with the path " + postPath,null);
path = postPath;
}
OutputStream out = client.getOutputStream();
httpOut = new HttpOutputStream(out, httpIn);
if (!handleAuthentication(httpIn, httpOut)) {
return;
}
HttpCommandProcessor processor = getProcessor(path.substring(1, path.length()));
if (processor == null)
{
HttpAdaptor.log(LogService.LOG_INFO,"No suitable command processor found, requesting from processor path " + path, null);
findUnknownElement(path, httpOut, httpIn);
}
else
{
Document document = processor.executeRequest(httpIn);
postProcess(httpOut, httpIn, document);
}
}
catch (Exception ex)
{
ex.printStackTrace();
HttpAdaptor.log(LogService.LOG_WARNING,"Exception during http request", ex);
if (httpOut != null)
{
try
{
postProcess(httpOut, httpIn, ex);
}
catch (IOException e)
{
HttpAdaptor.log(LogService.LOG_WARNING,"IOException during http request", e);
}
catch (JMException e)
{
HttpAdaptor.log(LogService.LOG_WARNING,"JMException during http request", e);
}
catch (RuntimeException rte)
{
HttpAdaptor.log(LogService.LOG_ERROR,"RuntimeException during http request", rte);
}
catch (Error er)
{
HttpAdaptor.log(LogService.LOG_ERROR,"Error during http request ", er);
}
catch (Throwable t)
{
HttpAdaptor.log(LogService.LOG_ERROR,"Throwable during http request ", t);
}
}
}
catch (Error ex)
{
HttpAdaptor.log(LogService.LOG_ERROR,"Error during http request ", ex);
}
finally
{
try
{
if (httpOut != null)
{
httpOut.flush();
}
// always close the socket
client.close();
}
catch (IOException e)
{
HttpAdaptor.log(LogService.LOG_WARNING,"Exception during request processing", e);
}
}
}
}
private static void log(int prio, String message, Throwable t){
if (HttpConnectorActivator.bc!=null){
ServiceReference logSR=HttpConnectorActivator.bc.getServiceReference(LogService.class.getName());
if (logSR!=null){
((LogService)HttpConnectorActivator.bc.getService(logSR)).log(prio, message, t);
}else{
System.out.println("No Log Service");
}
}else{
System.out.println(HttpAdaptor.class.getName()+".log: No bundleContext");
}
}
}