package org.apache.xmlrpc;

/*
 * The Apache Software License, Version 1.1
 *
 *
 * Copyright (c) 2001 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "XML-RPC" and "Apache Software Foundation" must
 *    not be used to endorse or promote products derived from this
 *    software without prior written permission. For written
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache",
 *    nor may "Apache" appear in their name, without prior written
 *    permission of the Apache Software Foundation.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 */

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.UnsupportedEncodingException;
import java.net.BindException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.EmptyStackException;
import java.util.Stack;
import java.util.StringTokenizer;
import java.util.Vector;

/**
 * A minimal web server that exclusively handles XML-RPC requests.
 *
 * @author <a href="mailto:hannes@apache.org">Hannes Wallnoefer</a>
 * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
 * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
 */
public class WebServer implements Runnable
{
    protected XmlRpcServer xmlrpc;

    protected ServerSocket serverSocket;
    protected Thread listener;
    protected Vector accept, deny;
    protected Stack threadpool;
    protected ThreadGroup runners;

    // Inputs to setupServerSocket()
    private InetAddress address;
    private int port;

    private boolean paranoid;

    protected static final byte[] ctype =
        toHTTPBytes("Content-Type: text/xml\r\n");
    protected static final byte[] clength =
        toHTTPBytes("Content-Length: ");
    protected static final byte[] newline = toHTTPBytes("\r\n");
    protected static final byte[] doubleNewline = toHTTPBytes("\r\n\r\n");
    protected static final byte[] conkeep =
        toHTTPBytes("Connection: Keep-Alive\r\n");
    protected static final byte[] conclose =
        toHTTPBytes("Connection: close\r\n");
    protected static final byte[] ok = toHTTPBytes(" 200 OK\r\n");
    protected static final byte[] server =
        toHTTPBytes("Server: Apache XML-RPC 1.0\r\n");
    protected static final byte[] wwwAuthenticate =
        toHTTPBytes("WWW-Authenticate: Basic realm=XML-RPC\r\n");

    private static final String HTTP_11 = "HTTP/1.1";
    private static final String STAR = "*";

    /**
     * This <em>can</em> be called from command line, but you'll have to edit
     * and recompile to change the server port or handler objects. By default,
     * it sets up the following responders:
     * <ul>
     *   <li> A java.lang.String object </li>
     *   <li> The java.lang.Math class (making its static methods callable via
     *        XML-RPC) </li>
     *   <li> An Echo handler that returns the argument array </li>
     * </ul>
     *
     * @see #addDefaultHandlers()
     */
    public static void main(String[] argv)
    {
        int p = determinePort(argv, 8080);
        // XmlRpc.setDebug (true);
        XmlRpc.setKeepAlive(true);
        WebServer webserver = new WebServer(p);

        try
        {
            webserver.addDefaultHandlers();
            webserver.start();
        }
        catch (Exception e)
        {
            System.err.println("Error running web server");
            e.printStackTrace();
            System.exit(1);
        }
    }

    /**
     * Examines command line arguments from <code>argv</code>.  If a
     * port may have been provided, parses that port (exiting with
     * error status if the port cannot be parsed).  If no port is
     * specified, defaults to <code>defaultPort</code>.
     *
     * @param defaultPort The port to use if none was specified.
     */
    protected static int determinePort(String[] argv, int defaultPort)
    {
        int port = defaultPort;
        if (argv.length > 0)
        {
            try
            {
                port = Integer.parseInt(argv[0]);
            }
            catch (NumberFormatException nfx)
            {
                System.err.println("Error parsing port number: " + argv[0]);
                System.err.println("Usage: java " + WebServer.class.getName()
                                   + " [port]");
                System.exit(1);
            }
        }
        return port;
    }

    /**
     * Creates a web server at the specified port number.
     */
    public WebServer(int port)
    {
        this(port, null);
    }

    /**
     * Creates a web server at the specified port number and IP address.
     */
    public WebServer(int port, InetAddress addr)
    {
        this(port, addr, new XmlRpcServer());
    }

    /**
     * Creates a web server at the specified port number and IP
     * address.
     */
    public WebServer(int port, InetAddress addr, XmlRpcServer xmlrpc)
    {
        this.address = addr;
        this.port = port;
        this.xmlrpc = xmlrpc;
        accept = new Vector();
        deny = new Vector();
        threadpool = new Stack();
        runners = new ThreadGroup("XML-RPC Runner");
    }

    /**
     * Returns the US-ASCII encoded byte representation of text for
     * HTTP use (as per section 2.2 of RFC 2068).
     */
    protected static final byte[] toHTTPBytes(String text)
    {
        try
        {
            return text.getBytes("US-ASCII");
        }
        catch (UnsupportedEncodingException e)
        {
            throw new Error(e.getMessage() +
                            ": HTTP requires US-ASCII encoding");
        }
    }

    /**
     * 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 <code>SSLServerSocket</code>).
     *
     * @param port
     * @param backlog
     * @param addr If <code>null</code>, binds to
     * <code>INADDR_ANY</code>, meaning that all network interfaces on
     * a multi-homed host will be listening.
     * @exception Exception Error creating listener socket.
     */
    protected ServerSocket createServerSocket(int port, int backlog,
            InetAddress addr)
            throws Exception
    {
        return new ServerSocket(port, 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 Exception
    {
        // 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.
        int attempt = 1;
        while (serverSocket == null)
        {
            try
            {
                serverSocket = createServerSocket(port, backlog, address);
            }
            catch (BindException e)
            {
                if (attempt == 10)
                {
                    throw e;
                }

                attempt++;
                Thread.sleep(1000);
            }
        }

        if (XmlRpc.debug)
        {
            StringBuffer msg = new StringBuffer();
            msg.append("Opened XML-RPC server socket for ");
            msg.append(address != null ? address.getHostName() : "localhost");
            msg.append(':').append(port);
            if (attempt > 1)
            {
                msg.append(" after ").append(attempt).append(" tries");
            }
            System.out.println(msg.toString());
        }

        // A socket timeout must be set.
        if (serverSocket.getSoTimeout() <= 0)
        {
            serverSocket.setSoTimeout(4096);
        }
    }

    /**
     * Spawns a new thread which binds this server to the port it's
     * configured to accept connections on.
     *
     * @see #run()
     */
    public void start()
    {
        // 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();
        }
    }

    /**
     * Register a handler object with this name. Methods of this objects will be
     * callable over XML-RPC as "name.method".
     */
    public void addHandler(String name, Object target)
    {
        xmlrpc.addHandler(name, target);
    }

    /**
     * Adds the bundled handlers to the server.  Called by {@link
     * #main(String[])}.
     */
    protected void addDefaultHandlers()
        throws Exception
    {
        // webserver.setParanoid (true);
        // webserver.acceptClient ("192.168.*.*");
        addHandler("string", "Welcome to XML-RPC!");
        addHandler("math", Math.class);
        addHandler("auth", new AuthDemo());
        addHandler("$default", new Echo());
        // XmlRpcClients can be used as Proxies in XmlRpcServers which is a
        // cool feature for applets.
        String url = "http://www.mailtothefuture.com:80/RPC2";
        addHandler("mttf", new XmlRpcClient(url));
        addHandler("system", new SystemHandler(xmlrpc));
    }

    /**
     * Remove a handler object that was previously registered with this server.
     */
    public void removeHandler(String name)
    {
        xmlrpc.removeHandler(name);
    }

    /**
     * Switch client filtering on/off.
     * @see #acceptClient(java.lang.String)
     * @see #denyClient(java.lang.String)
     */
    public void setParanoid(boolean p)
    {
        paranoid = p;
    }

    /**
     * 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.
     *
     * @see #denyClient(java.lang.String)
     * @see #setParanoid(boolean)
     */
    public void acceptClient(String address) throws IllegalArgumentException
    {
        try
        {
            AddressMatcher m = new AddressMatcher(address);
            accept.addElement(m);
        }
        catch (Exception x)
        {
            throw new IllegalArgumentException("\"" + address
                    + "\" does not represent a valid IP address");
        }
    }

    /**
     * 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.
     *
     * @see #acceptClient(java.lang.String)
     * @see #setParanoid(boolean)
     */
    public void denyClient(String address) throws IllegalArgumentException
    {
        try
        {
            AddressMatcher m = new AddressMatcher(address);
            deny.addElement(m);
        }
        catch (Exception x)
        {
            throw new IllegalArgumentException("\"" + address
                    + "\" does not represent a valid IP address");
        }
    }

    /**
     * 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 address[] = s.getInetAddress().getAddress();
        for (int i = 0; i < l; i++)
        {
            AddressMatcher match = (AddressMatcher)deny.elementAt(i);
            if (match.matches(address))
            {
                return false;
            }
        }
        l = accept.size();
        for (int i = 0; i < l; i++)
        {
            AddressMatcher match = (AddressMatcher)accept.elementAt(i);
            if (match.matches(address))
            {
                return true;
            }
        }
        return false;
    }

    /**
     * DEPRECATED: Do not use this method, it will be removed soon.
     * Use {@link #allowConnection(Socket)} instead.
     *
     * @deprecated Use allowConnection(Socket) instead.
     * @see #allowConnection(Socket)
     */
    protected boolean checkSocket(Socket s)
    {
        return allowConnection(s);
    }

    /**
     * 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
     * <code>UnknownHostException</code> or <code>BindException</code>
     * with the vanilla web server.
     *
     * @see #start()
     * @see #shutdown()
     */
    public void run()
    {
        try
        {
            setupServerSocket(50);
        }
        catch (Exception e)
        {
            listener = null;
            e.printStackTrace();
            throw new RuntimeException(e.getMessage());
        }

        try
        {
            while (listener != null)
            {
                try
                {
                    Socket socket = serverSocket.accept();
                    try
                    {
                        socket.setTcpNoDelay(true);
                    }
                    catch (SocketException socketOptEx)
                    {
                        System.err.println(socketOptEx);
                    }

                    if (allowConnection(socket))
                    {
                        Runner runner = getRunner();
                        runner.handle(socket);
                    }
                    else
                    {
                        socket.close();
                    }
                }
                catch (InterruptedIOException checkState)
                {
                    // Timeout while waiting for a client (from
                    // SO_TIMEOUT)...try again if still listening.
                }
                catch (Exception ex)
                {
                    System.err.println("Exception in XML-RPC listener loop ("
                            + ex + ").");
                    if (XmlRpc.debug)
                    {
                        ex.printStackTrace();
                    }
                }
                catch (Error err)
                {
                    System.err.println("Error in XML-RPC listener loop ("
                            + err + ").");
                    err.printStackTrace();
                }
            }
        }
        catch (Exception exception)
        {
            System.err.println("Error accepting XML-RPC connections ("
                    + exception + ").");
            if (XmlRpc.debug)
            {
                exception.printStackTrace();
            }
        }
        finally
        {
            if (serverSocket != null)
            {
                try
                {
                    serverSocket.close();
                    if (XmlRpc.debug)
                    {
                        System.out.print("Closed XML-RPC server socket");
                    }
                    serverSocket = null;
                }
                catch (IOException e)
                {
                    e.printStackTrace();
                }
            }

            // Shutdown our Runner-based threads
            if (runners != null)
            {
                ThreadGroup g = runners;
                runners = null;
                try
                {
                    g.interrupt();
                }
                catch (Exception e)
                {
                    System.err.println(e);
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 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();
        }
    }

    /**
     *
     * @return
     */
    protected Runner getRunner()
    {
        try
        {
            return (Runner)threadpool.pop();
        }
        catch (EmptyStackException empty)
        {
            if (runners.activeCount () > 255)
            {
                throw new RuntimeException("System overload");
            }
            return new Runner();
        }
    }

    /**
     *
     * @param runner
     */
    void releaseRunner(Runner runner)
    {
        threadpool.push(runner);
    }

    /**
     * Responsible for handling client connections.
     */
    class Runner implements Runnable
    {
        Thread thread;
        Connection con;
        int count;

        /**
         * Handles the client connection on <code>socket</code>.
         *
         * @param socket The source to read the client's request from.
         */
        public synchronized void handle(Socket socket) throws IOException
        {
            con = new Connection(socket);
            count = 0;
            if (thread == null || !thread.isAlive())
            {
                thread = new Thread(runners, this);
                thread.start();
            }
            else
            {
                this.notify();
            }
        }

        /**
         * Delegates to <code>con.run()</code>.
         */
        public void run()
        {
            while (con != null && Thread.currentThread() == thread)
            {
                con.run();
                count++;
                con = null;

                if (count > 200 || threadpool.size() > 20)
                {
                    return;
                }
                synchronized(this)
                {
                    releaseRunner(this);
                    try
                    {
                        this.wait();
                    }
                    catch (InterruptedException ir)
                    {
                        Thread.currentThread().interrupt();
                    }
                }
            }
        }
    }

    /**
     *
     */
    class Connection implements Runnable
    {
        private Socket socket;
        private BufferedInputStream input;
        private BufferedOutputStream output;
        private String user, password;
        byte[] buffer;

        /**
         *
         * @param socket
         * @throws IOException
         */
        public Connection (Socket socket) throws IOException
        {
            // set read timeout to 30 seconds
            socket.setSoTimeout (30000);

            this.socket = socket;
            input = new BufferedInputStream(socket.getInputStream());
            output = new BufferedOutputStream(socket.getOutputStream());
        }

        /**
         *
         */
        public void run()
        {
            try
            {
                boolean keepAlive = false;

                do
                {
                    // reset user authentication
                    user = null;
                    password = null;
                    String line = readLine();
                    // Netscape sends an extra \n\r after bodypart, swallow it
                    if (line != null && line.length() == 0)
                    {
                        line = readLine();
                    }
                    if (XmlRpc.debug)
                    {
                        System.out.println(line);
                    }
                    int contentLength = -1;

                    // tokenize first line of HTTP request
                    StringTokenizer tokens = new StringTokenizer(line);
                    String method = tokens.nextToken();
                    String uri = tokens.nextToken();
                    String httpVersion = tokens.nextToken();
                    keepAlive = XmlRpc.getKeepAlive()
                            && HTTP_11.equals(httpVersion);
                    do
                    {
                        line = readLine();
                        if (line != null)
                        {
                            if (XmlRpc.debug)
                            {
                                System.out.println(line);
                            }
                            String lineLower = line.toLowerCase();
                            if (lineLower.startsWith("content-length:"))
                            {
                                contentLength = Integer.parseInt(
                                        line.substring(15).trim());
                            }
                            if (lineLower.startsWith("connection:"))
                            {
                                keepAlive = XmlRpc.getKeepAlive() &&
                                        lineLower.indexOf("keep-alive") > -1;
                            }
                            if (lineLower.startsWith("authorization: basic "))
                            {
                                parseAuth (line);
                            }
                        }
                    }
                    while (line != null && line.length() != 0);

                    if ("POST".equalsIgnoreCase(method))
                    {
                        ServerInputStream sin = new ServerInputStream(input,
                                contentLength);
                        try
                        {
                            byte[] result = xmlrpc.execute(sin, user, password);
                            writeResponse(result, httpVersion, keepAlive);
                        }
                        catch (AuthenticationFailed unauthorized)
                        {
                            keepAlive = false;
                            writeUnauthorized(httpVersion, method);
                        }
                    }
                    else
                    {
                        keepAlive = false;
                        writeBadRequest(httpVersion, method);
                    }
                    output.flush();
                }
                while (keepAlive);
            }
            catch (Exception exception)
            {
                System.err.println(exception);
                if (XmlRpc.debug)
                {
                    exception.printStackTrace();
                }
            }
            finally
            {
                try
                {
                    if (socket != null)
                    {
                        socket.close();
                    }
                }
                catch (IOException ignore)
                {
                }
            }
        }

        /**
         *
         * @return
         * @throws IOException
         */
        private String readLine() throws IOException
        {
            if (buffer == null)
            {
                buffer = new byte[2048];
            }
            int next;
            int count = 0;
            for (;;)
            {
                next = input.read();
                if (next < 0 || next == '\n')
                {
                    break;
                }
                if (next != '\r')
                {
                    buffer[count++] = (byte) next;
                }
                if (count >= buffer.length)
                {
                    throw new IOException("HTTP Header too long");
                }
            }
            return new String(buffer, 0, count);
        }

        /**
         *
         * @param line
         */
        private void parseAuth(String line)
        {
            try
            {
                byte[] c = Base64.decode(toHTTPBytes(line.substring(21)));
                String str = new String(c);
                int col = str.indexOf(':');
                user = str.substring(0, col);
                password = str.substring(col + 1);
            }
            catch (Throwable ignore)
            {
            }
        }

        private void writeResponse(byte[] payload, String httpVersion,
                                   boolean keepAlive)
            throws IOException
        {
            output.write(toHTTPBytes(httpVersion));
            output.write(ok);
            output.write(server);
            output.write(keepAlive ? conkeep : conclose);
            output.write(ctype);
            output.write(clength);
            output.write(toHTTPBytes(Integer.toString(payload.length)));
            output.write(doubleNewline);
            output.write(payload);
        }

        private void writeBadRequest(String httpVersion, String httpMethod)
            throws IOException
        {
            output.write(toHTTPBytes(httpVersion));
            output.write(toHTTPBytes(" 400 Bad Request"));
            output.write(newline);
            output.write(server);
            output.write(newline);
            output.write(toHTTPBytes("Method " + httpMethod +
                                     " not implemented (try POST)"));
        }

        private void writeUnauthorized(String httpVersion, String httpMethod)
            throws IOException
        {
            output.write(toHTTPBytes(httpVersion));
            output.write(toHTTPBytes(" 401 Unauthorized"));
            output.write(newline);
            output.write(server);
            output.write(wwwAuthenticate);
            output.write(newline);
            output.write(toHTTPBytes("Method " + httpMethod + " requires a " +
                                     "valid user name and password"));
        }
    }

    /**
     *
     */
    class AddressMatcher
    {
        int pattern[];

        /**
         *
         * @param address
         * @throws Exception
         */
        public AddressMatcher(String address) throws Exception
        {
            pattern = new int[4];
            StringTokenizer st = new StringTokenizer(address, ".");
            if (st.countTokens() != 4)
            {
                throw new Exception("\"" + address
                        + "\" does not represent a valid IP address");
            }
            for (int i = 0; i < 4; i++)
            {
                String next = st.nextToken();
                if (STAR.equals(next))
                {
                    pattern[i] = 256;
                }
                else
                {
                    pattern[i] = (byte) Integer.parseInt(next);
                }
            }
        }

        /**
         *
         * @param address
         * @return
         */
        public boolean matches (byte address[])
        {
            for (int i = 0; i < 4; i++)
            {
                if (pattern[i] > 255)// wildcard
                {
                    continue;
                }
                if (pattern[i] != address[i])
                {
                    return false;
                }
            }
            return true;
        }
    }
}
