org.apache.xmlrpc.webserver.WebServer Maven / Gradle / Ivy
/*
* Copyright 1999,2005 The Apache Software Foundation.
*
* 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 org.apache.xmlrpc.webserver;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.BindException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xmlrpc.server.XmlRpcStreamServer;
import org.apache.xmlrpc.util.ThreadPool;
/**
* The {@link WebServer} is a minimal HTTP server, that might be used
* as an embedded web server.
* Use of the {@link WebServer} has grown very popular amongst users
* of Apache XML-RPC. Why this is the case, can hardly be explained,
* because the {@link WebServer} is at best a workaround, compared to
* full blown servlet engines like Tomcat or Jetty. For example, under
* heavy load it will almost definitely be slower than a real servlet
* engine, because it does neither support proper keepalive (multiple
* requests per physical connection) nor chunked mode (in other words,
* it cannot stream requests).
* If you still insist in using the {@link WebServer}, it is
* recommended to use its subclass, the {@link ServletWebServer} instead,
* which offers a minimal subset of the servlet API. In other words,
* you keep yourself the option to migrate to a real servlet engine
* later.
* Use of the {@link WebServer} goes roughly like this: First of all,
* create a property file (for example "MyHandlers.properties") and
* add it to your jar file. The property keys are handler names and
* the property values are the handler classes. Once that is done,
* create an instance of WebServer:
*
* final int portNumber = 8088;
* final String propertyFile = "MyHandler.properties";
*
* PropertyHandlerMapping mapping = new PropertyHandlerMapping();
* ClassLoader cl = Thread.currentThread().getContextClassLoader();
* mapping.load(cl, propertyFile);
* WebServer webServer = new WebServer(port);
* XmlRpcServerConfigImpl config = new XmlRpcServerConfigImpl();
* XmlRpcServer server = server.getXmlRpcServer();
* server.setConfig(config);
* server.setHandlerMapping(mapping);
* server.start();
*
*/
public class WebServer implements Runnable {
private static final Log log = LogFactory.getLog(WebServer.class);
private class AddressMatcher {
private final int pattern[];
AddressMatcher(String pAddress) {
try {
pattern = new int[4];
StringTokenizer st = new StringTokenizer(pAddress, ".");
if (st.countTokens() != 4) {
throw new IllegalArgumentException();
}
for (int i = 0; i < 4; i++) {
String next = st.nextToken();
if ("*".equals(next)) {
pattern[i] = 256;
} else {
pattern[i] = Integer.parseInt(next);
}
}
} catch (Exception e) {
throw new IllegalArgumentException("\"" + pAddress
+ "\" does not represent a valid IP address");
}
}
boolean matches(byte[] pAddress) {
for (int i = 0; i < 4; i++) {
if (pattern[i] > 255) {
continue; // Wildcard
}
if (pattern[i] != pAddress[i]) {
return false;
}
}
return true;
}
}
protected ServerSocket serverSocket;
private Thread listener;
private ThreadPool pool;
protected final List accept = new ArrayList();
protected final List deny = new ArrayList();
protected final XmlRpcStreamServer server = newXmlRpcStreamServer();
protected XmlRpcStreamServer newXmlRpcStreamServer(){
return new ConnectionServer();
}
// Inputs to setupServerSocket()
private InetAddress address;
private int port;
private boolean paranoid;
static final String HTTP_11 = "HTTP/1.1";
/** Creates a web server at the specified port number.
* @param pPort Port number; 0 for a random port, choosen by the
* operating system.
*/
public WebServer(int pPort) {
this(pPort, null);
}
/** Creates a web server at the specified port number and IP address.
* @param pPort Port number; 0 for a random port, choosen by the
* operating system.
* @param pAddr Local IP address; null for all available IP addresses.
*/
public WebServer(int pPort, InetAddress pAddr) {
address = pAddr;
port = pPort;
}
/**
* Factory method to manufacture the server socket. Useful as a
* hook method for subclasses to override when they desire
* different flavor of socket (i.e. a SSLServerSocket
).
*
* @param pPort Port number; 0 for a random port, choosen by the operating
* system.
* @param backlog
* @param addr If null
, binds to
* INADDR_ANY
, meaning that all network interfaces on
* a multi-homed host will be listening.
* @exception IOException Error creating listener socket.
*/
protected ServerSocket createServerSocket(int pPort, int backlog, InetAddress addr)
throws IOException {
return new ServerSocket(pPort, backlog, addr);
}
/**
* Initializes this server's listener socket with the specified
* attributes, assuring that a socket timeout has been set. The
* {@link #createServerSocket(int, int, InetAddress)} method can
* be overridden to change the flavor of socket used.
*
* @see #createServerSocket(int, int, InetAddress)
*/
private synchronized void setupServerSocket(int backlog) throws IOException {
// Since we can't reliably set SO_REUSEADDR until JDK 1.4 is
// the standard, try to (re-)open the server socket several
// times. Some OSes (Linux and Solaris, for example), hold on
// to listener sockets for a brief period of time for security
// reasons before relinquishing their hold.
for (int i = 1; ; i++) {
try {
serverSocket = createServerSocket(port, backlog, address);
// A socket timeout must be set.
if (serverSocket.getSoTimeout() <= 0) {
serverSocket.setSoTimeout(4096);
}
return;
} catch (BindException e) {
if (i == 10) {
throw e;
} else {
long waitUntil = System.currentTimeMillis();
for (;;) {
long l = waitUntil - System.currentTimeMillis();
if (l > 0) {
try {
Thread.sleep(l);
} catch (InterruptedException ex) {
}
}
}
}
}
}
}
/**
* Spawns a new thread which binds this server to the port it's
* configured to accept connections on.
*
* @see #run()
* @throws IOException Binding the server socket failed.
*/
public void start() throws IOException {
setupServerSocket(50);
// The listener reference is released upon shutdown().
if (listener == null) {
listener = new Thread(this, "XML-RPC Weblistener");
// Not marked as daemon thread since run directly via main().
listener.start();
}
}
/**
* Switch client filtering on/off.
* @param pParanoid True to enable filtering, false otherwise.
* @see #acceptClient(java.lang.String)
* @see #denyClient(java.lang.String)
*/
public void setParanoid(boolean pParanoid) {
paranoid = pParanoid;
}
/** Add an IP address to the list of accepted clients. The parameter can
* contain '*' as wildcard character, e.g. "192.168.*.*". You must call
* setParanoid(true) in order for this to have any effect.
* @param pAddress The IP address being enabled.
* @see #denyClient(java.lang.String)
* @see #setParanoid(boolean)
* @throws IllegalArgumentException Parsing the address failed.
*/
public void acceptClient(String pAddress) {
accept.add(new AddressMatcher(pAddress));
}
/**
* Add an IP address to the list of denied clients. The parameter can
* contain '*' as wildcard character, e.g. "192.168.*.*". You must call
* setParanoid(true) in order for this to have any effect.
* @param pAddress The IP address being disabled.
* @see #acceptClient(java.lang.String)
* @see #setParanoid(boolean)
* @throws IllegalArgumentException Parsing the address failed.
*/
public void denyClient(String pAddress) {
deny.add(new AddressMatcher(pAddress));
}
/**
* Checks incoming connections to see if they should be allowed.
* If not in paranoid mode, always returns true.
*
* @param s The socket to inspect.
* @return Whether the connection should be allowed.
*/
protected boolean allowConnection(Socket s) {
if (!paranoid) {
return true;
}
int l = deny.size();
byte addr[] = s.getInetAddress().getAddress();
for (int i = 0; i < l; i++) {
AddressMatcher match = (AddressMatcher) deny.get(i);
if (match.matches(addr))
{
return false;
}
}
l = accept.size();
for (int i = 0; i < l; i++) {
AddressMatcher match = (AddressMatcher) accept.get(i);
if (match.matches(addr)) {
return true;
}
}
return false;
}
protected ThreadPool.Task newTask(WebServer pServer, XmlRpcStreamServer pXmlRpcServer,
Socket pSocket) throws IOException {
return new Connection(pServer, pXmlRpcServer, pSocket);
}
/**
* Listens for client requests until stopped. Call {@link
* #start()} to invoke this method, and {@link #shutdown()} to
* break out of it.
*
* @throws RuntimeException Generally caused by either an
* UnknownHostException
or BindException
* with the vanilla web server.
*
* @see #start()
* @see #shutdown()
*/
public void run() {
pool = new ThreadPool(server.getMaxThreads(), "XML-RPC");
try {
while (listener != null) {
try {
Socket socket = serverSocket.accept();
try {
socket.setTcpNoDelay(true);
} catch (SocketException socketOptEx) {
log(socketOptEx);
}
try {
if (allowConnection(socket)) {
final ThreadPool.Task task = newTask(this, server, socket);
if (pool.startTask(task)) {
socket = null;
} else {
log("Maximum load of " + pool.getMaxThreads()
+ " exceeded, rejecting client");
}
}
} finally {
if (socket != null) { try { socket.close(); } catch (Throwable ignore) {} }
}
} catch (InterruptedIOException checkState) {
// Timeout while waiting for a client (from
// SO_TIMEOUT)...try again if still listening.
} catch (Throwable t) {
log(t);
}
}
} finally {
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e) {
log(e);
}
}
// Shutdown our Runner-based threads
pool.shutdown();
}
}
/**
* Stop listening on the server port. Shutting down our {@link
* #listener} effectively breaks it out of its {@link #run()}
* loop.
*
* @see #run()
*/
public synchronized void shutdown() {
// Stop accepting client connections
if (listener != null) {
Thread l = listener;
listener = null;
l.interrupt();
pool.shutdown();
}
}
/** Returns the port, on which the web server is running.
* This method may be invoked after {@link #start()} only.
* @return Servers port number
*/
public int getPort() { return serverSocket.getLocalPort(); }
/** Logs an error.
* @param pError The error being logged.
*/
public void log(Throwable pError) {
log.error(pError.getMessage(), pError);
}
/** Logs a message.
* @param pMessage The being logged.
*/
public synchronized void log(String pMessage) {
log.error(pMessage);
}
/** Returns the {@link org.apache.xmlrpc.server.XmlRpcServer}.
* @return The server object.
*/
public XmlRpcStreamServer getXmlRpcServer() {
return server;
}
}