/*
 * SsiInvokerServlet.java
 * $Header: /home/cvs/jakarta-tomcat-4.0/catalina/src/share/org/apache/catalina/servlets/Attic/SsiInvokerServlet.java,v 1.8.2.1 2001/10/22 21:35:48 bip Exp $
 * $Revision: 1.8.2.1 $
 * $Date: 2001/10/22 21:35:48 $
 *
 * ====================================================================
 *
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 1999 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 acknowlegement:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowlegement may appear in the software itself,
 *    if and wherever such third-party acknowlegements normally appear.
 *
 * 4. The names "The Jakarta Project", "Tomcat", 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 names without prior written
 *    permission of the Apache Group.
 *
 * 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/>.
 *
 * [Additional notices, if required by prior licensing conditions]
 *
 */

package org.apache.catalina.servlets;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.Date;
import java.util.TimeZone;
import java.util.Locale;
import java.util.StringTokenizer;
import java.text.SimpleDateFormat;
import javax.servlet.ServletException;
import javax.servlet.ServletContext;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.naming.directory.DirContext;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.NamingException;
import javax.naming.InitialContext;
import org.apache.catalina.Globals;
import org.apache.catalina.util.RequestUtil;
import org.apache.catalina.util.ssi.SsiCommand;
import org.apache.catalina.util.ssi.SsiMediator;
import org.apache.catalina.util.ssi.ServletOutputStreamWrapper;

/**
 * Servlet to process SSI requests within a webpage.
 * Mapped to a path from within web.xml.
 *
 * @author Bip Thelin
 * @author Amy Roh
 * @version $Revision: 1.8.2.1 $, $Date: 2001/10/22 21:35:48 $
 */
public final class SsiInvokerServlet extends HttpServlet {
    /** Debug level for this servlet. */
    private int debug = 0;

    /** Should the output be buffered. */
    private boolean buffered = false;

    /** Expiration time in seconds for the doc. */
    private Long expires = null;

    /** Should we ignore unsupported/misspelled SSI Directives */
    private boolean ignoreUnsupportedDirective = false;

    /** virtual path can be webapp-relative */
    private boolean isVirtualWebappRelative = false;

    /** The Mediator object for the SsiCommands. */
    private static SsiMediator ssiMediator = null;

    /** The start pattern */
    private final static byte[] bStart = {
        (byte)'<',(byte)'!',(byte)'-',(byte)'-',(byte)'#'
    };

    /** The end pattern */
    private final static byte[] bEnd = {
        (byte)'-',(byte)'-',(byte)'>'
    };

    //----------------- Public methods.

    /**
     * Initialize this servlet.
     * @exception ServletException if an error occurs
     */
    public void init() throws ServletException {
        String value = null;
        try {
            value = getServletConfig().getInitParameter("debug");
            debug = Integer.parseInt(value);
        } catch (Throwable t) {
            ;
        }

        try {
            value = getServletConfig().getInitParameter("isVirtualWebappRelative");
            isVirtualWebappRelative = Integer.parseInt(value) > 0 ? true : false;
        } catch (Throwable t) {
            ;
        }

        try {
            value = getServletConfig().getInitParameter("ignoreUnsupportedDirective");
            ignoreUnsupportedDirective = Integer.parseInt(value) > 0 ? true : false;
        } catch (Throwable t) {
            ;
        }

        try {
            value = getServletConfig().getInitParameter("expires");
            expires = Long.valueOf(value);
        } catch (NumberFormatException e) {
            expires = null;
            log("Invalid format for expires initParam; expected integer (seconds)");
        } catch (Throwable t) {
            ;
        }
        try {
            value = getServletConfig().getInitParameter("buffered");
            buffered = Integer.parseInt(value) > 0 ? true : false;
        } catch (Throwable t) {
            ;
        }
        if (debug > 0)
            log("SsiInvokerServlet.init() SSI invoker started with 'debug'="
                + debug);
    }

    /**
     * Process and forward the GET request
     * to our <code>requestHandler()</code>.
     *
     * @param req a value of type 'HttpServletRequest'
     * @param res a value of type 'HttpServletResponse'
     * @exception IOException if an error occurs
     * @exception ServletException if an error occurs
     */
    public void doGet(HttpServletRequest req, HttpServletResponse res)
        throws IOException, ServletException {

        if (debug > 0)
            log("SsiInvokerServlet.doGet()");
        requestHandler(req, res);
    }

    /**
     * Process and forward the POST request
     * to our <code>requestHandler()</code>.
     *
     * @param req a value of type 'HttpServletRequest'
     * @param res a value of type 'HttpServletResponse'
     * @exception IOException if an error occurs
     * @exception ServletException if an error occurs
     */
    public void doPost(HttpServletRequest req, HttpServletResponse res)
        throws IOException, ServletException {

        if (debug > 0)
            log("SsiInvokerServlet.doPut()");
        requestHandler(req, res);
    }

    //----------------- Private methods.

    /**
     * Process our request and locate right SSI command.
     * @param req a value of type 'HttpServletRequest'
     * @param res a value of type 'HttpServletResponse'
     */
    private void requestHandler(HttpServletRequest req,
                                HttpServletResponse res)
        throws IOException, ServletException {

        ServletContext servletContext = getServletContext();
        String path = getRelativePath(req);
            URL resource = servletContext.getResource(path);

        if (debug > 0)
            log("SsiInvokerServlet.requestHandler()\n" +
                "Serving " + (buffered ? "buffered " : "unbuffered ") +
                "resource '" + path + "'");

        // Exclude any resource in the /WEB-INF and /META-INF subdirectories
        // (the "toUpperCase()" avoids problems on Windows systems)
        if ((path == null)
            || path.toUpperCase().startsWith("/WEB-INF")
            || path.toUpperCase().startsWith("/META-INF")) {

            res.sendError(res.SC_NOT_FOUND, path);
            return;
        }

        if (resource==null) {
            res.sendError(res.SC_NOT_FOUND, path);
            return;
        }

        res.setContentType("text/html;charset=UTF-8");

        if (expires != null) {
            res.setDateHeader("Expires", (
                new java.util.Date()).getTime() + expires.longValue() * 1000);
        }

        OutputStream out = null;
        URLConnection resourceInfo = resource.openConnection();
        InputStream resourceInputStream = resourceInfo.getInputStream();

        InputStream in = new BufferedInputStream(resourceInputStream, 4096);
        ByteArrayOutputStream soonOut = new ByteArrayOutputStream(resourceInfo.getContentLength());

        StringBuffer command = new StringBuffer();
        byte buf[] = new byte[4096];
        int len = 0, bIdx = 0;
        char ch;
        boolean inside = false;
        SsiCommand ssiCommand = null;
        String strCmd;
        String[] strParamType;
        String[] strParam;

        if (buffered)
            out = (OutputStream)new ServletOutputStreamWrapper();
        else
            out = res.getOutputStream();

        if (ssiMediator == null)
            ssiMediator = new SsiMediator(req, res, out, servletContext, debug, path, isVirtualWebappRelative);
        else
            ssiMediator.flush(req, res, out, servletContext, path, isVirtualWebappRelative);

        while ((len = in.read(buf)) != -1)
            soonOut.write(buf, 0, len);

        soonOut.close();
        byte[] unparsed = soonOut.toByteArray();
        soonOut = null; buf = null;
        while (bIdx < unparsed.length) {
            if (!inside) {
                if (unparsed[bIdx] == bStart[0]&& byteCmp(unparsed, bIdx, bStart)) {
                    inside = true;
                    bIdx += bStart.length;
                    command.delete(0, command.length());
                    continue;
                }
                out.write(unparsed[bIdx]);
                bIdx++;
            } else {
                if (unparsed[bIdx] == bEnd[0]&& byteCmp(unparsed, bIdx, bEnd)) {
                    inside = false;
                    bIdx += bEnd.length;
                    try {
                        strCmd = parseCmd(command);
                    } catch (IndexOutOfBoundsException ex) {
                        out.write(ssiMediator.getError());
                        continue;
                    }
                    strCmd = parseCmd(command);
                    strParamType = parseParamType(command, strCmd.length());
                    strParam = parseParam(command, strCmd.length());

                            if(debug > 0)
                                log("Serving SSI resource: " + strCmd);

                    ssiCommand = ssiMediator.getCommand(strCmd);
                    if (ssiCommand != null && strParamType.length==strParam.length&& strParamType.length>0) {
                        if (ssiCommand.isPrintable()) {
                            out.write((ssiCommand.getStream(strParamType, strParam)).getBytes());
                        } else
                            ssiCommand.process(strParamType, strParam);
                    } else if(ignoreUnsupportedDirective && ssiCommand==null) {
                        ;
                    } else {
                        out.write(ssiMediator.getError());
                    }
                    continue;
                }
                command.append((char)unparsed[bIdx]);
                bIdx++;
            }
        }
        if (buffered)
            ((ServletOutputStreamWrapper)out).writeTo(res.getOutputStream());

        out = null;
    }

    /**
     * Parse a StringBuffer and take out the command token.
     * Called from <code>requestHandler</code>
     * @param cmd a value of type 'StringBuffer'
     * @return a value of type 'String'
     */
    private String parseCmd(StringBuffer cmd)
        throws IndexOutOfBoundsException {

        String modString = ((cmd.toString()).trim()).toLowerCase();
        return modString.substring(0, modString.indexOf(' '));
    }

    /**
     * Parse a StringBuffer and take out the param type token.
     * Called from <code>requestHandler</code>
     * @param cmd a value of type 'StringBuffer'
     * @return a value of type 'String[]'
     */
    private String[] parseParamType(StringBuffer cmd, int start) {
        int bIdx = start;
        int i = 0;
        int quotes = 0;
        boolean inside = false;
        StringBuffer retBuf = new StringBuffer();

        while(bIdx < cmd.length()) {
            if(!inside) {
                while(bIdx < cmd.length()&&isSpace(cmd.charAt(bIdx)))
                    bIdx++;

                if(bIdx>=cmd.length())
                    break;

                inside=!inside;
            } else {
                while(bIdx < cmd.length()&&cmd.charAt(bIdx)!='=') {
                    retBuf.append(cmd.charAt(bIdx));
                    bIdx++;
                }

                retBuf.append('"');
                inside=!inside;
                quotes=0;

                while(bIdx < cmd.length()&&quotes!=2) {
                    if(cmd.charAt(bIdx)=='"')
                            quotes++;

                    bIdx++;
                }
            }
        }

        StringTokenizer str = new StringTokenizer(retBuf.toString(), "\"");
        String[] retString = new String[str.countTokens()];

        while(str.hasMoreTokens()) {
            retString[i++] = str.nextToken().trim();
        }

        return retString;
    }

    /**
     * Parse a StringBuffer and take out the param token.
     * Called from <code>requestHandler</code>
     * @param cmd a value of type 'StringBuffer'
     * @return a value of type 'String[]'
     */
    private String[] parseParam(StringBuffer cmd, int start) {
        int bIdx = start;
        int i = 0;
        int quotes = 0;
        boolean inside = false;
        StringBuffer retBuf = new StringBuffer();

        while(bIdx < cmd.length()) {
            if(!inside) {
                while(bIdx < cmd.length()&&
                      cmd.charAt(bIdx)!='"')
                    bIdx++;

                if(bIdx>=cmd.length())
                    break;

                inside=!inside;
            } else {
                while(bIdx < cmd.length() && cmd.charAt(bIdx)!='"') {
                    retBuf.append(cmd.charAt(bIdx));
                    bIdx++;
                }

                retBuf.append('"');
                inside=!inside;
            }

            bIdx++;
        }

        StringTokenizer str = new StringTokenizer(retBuf.toString(), "\"");
        String[] retString = new String[str.countTokens()];

        while(str.hasMoreTokens()) {
            retString[i++] = str.nextToken();
        }

        return retString;
    }

    /**
     * Check the input param 'buf' if it matches with the
     * pattern 'pattern'. If it does return true.
     * @param buf a value of type 'byte[]'
     * @param bIdx a value of type 'int'
     * @param pattern a value of type 'byte[]'
     * @return a value of type 'boolean'
     */
    private boolean byteCmp(byte[] buf, int bIdx, byte[] pattern) {
        for (int i = 0; i < pattern.length; i++)
            if (buf[bIdx + i] != pattern[i])
                return false;
        return true;
    }

    private boolean isSpace(char c) {
        return c==' '||c=='\n'||c=='\t'||c=='\r';
    }

    //----------------- Taken from DefaultServlet.java

    /**
     * Return the relative path associated with this servlet.
     * @param request The servlet request we are processing
     */
    private String getRelativePath(HttpServletRequest request) {
        // Are we being processed by a RequestDispatcher.include()?
        if (request.getAttribute("javax.servlet.include.request_uri") != null) {
            String result =
                (String)request.getAttribute("javax.servlet.include.path_info");
            if (result == null)
                result =
                    (String)request.getAttribute("javax.servlet.include.servlet_path");
            if ((result == null) || (result.equals("")))
                result = "/";
            return (result);
        }
        // No, extract the desired path directly from the request
        String result = request.getPathInfo();
        if (result == null) {
            result = request.getServletPath();
        }
        if ((result == null) || (result.equals(""))) {
            result = "/";
        }
        return normalize(result);
    }

    /**
     * Return a context-relative path, beginning with a "/", that represents
     * the canonical version of the specified path after ".." and "." elements
     * are resolved out.  If the specified path attempts to go outside the
     * boundaries of the current context (i.e. too many ".."
     * path elements are present), return <code>null</code> instead.
     * @param path Path to be normalized
     */
    private String normalize(String path) {
        if (path == null)
            return null;

        // Create a place for the normalized path
        String normalized = path;

        /*
         * Commented out -- already URL-decoded in StandardContextMapper
         * Decoding twice leaves the container vulnerable to %25 --> '%'
         * attacks.
         *
         * if (normalized.indexOf('%') >= 0)
         *     normalized = RequestUtil.URLDecode(normalized, "UTF8");
         */

        if (normalized == null)
            return (null);

        // Normalize the slashes and add leading slash if necessary
        if (normalized.indexOf('\\') >= 0)
            normalized = normalized.replace('\\', '/');
        if (!normalized.startsWith("/"))
            normalized = "/" + normalized;
        // Resolve occurrences of "//" in the normalized path
        while (true) {
            int index = normalized.indexOf("//");
            if (index < 0)
                break;
            normalized = normalized.substring(0, index) +
                normalized.substring(index + 1);
        }
        // Resolve occurrences of "/./" in the normalized path
        while (true) {
            int index = normalized.indexOf("/./");
            if (index < 0)
                break;
            normalized = normalized.substring(0, index) +
                normalized.substring(index + 2);
        }
        // Resolve occurrences of "/../" in the normalized path
        while (true) {
            int index = normalized.indexOf("/../");
            if (index < 0)
                break;
            if (index == 0)
                return (null); // Trying to go outside our context
            int index2 = normalized.lastIndexOf('/', index - 1);
            normalized = normalized.substring(0, index2) +
                normalized.substring(index + 3);
        }
        // Return the normalized path that we have completed
        return (normalized);
    }
}
