Package org.geoserver.filters

Source Code of org.geoserver.filters.ReverseProxyFilter$CacheingResponseWrapper

/* Copyright (c) 2001 - 2008 TOPP - www.openplans.org. All rights reserved.
* This code is licensed under the GPL 2.0 license, availible at the root
* application directory.
*/
package org.geoserver.filters;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.URL;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

import org.geoserver.config.GeoServer;
import org.geoserver.config.GeoServerInfo;
import org.geoserver.ows.util.RequestUtils;
import org.geoserver.platform.GeoServerExtensions;
import org.geotools.util.logging.Logging;

/**
* Servlet Filter that performs URL translation on content based on configured mime types.
* <p>
* This filter does the job of a content filtering reverse proxy, like apache2 <code>mod_html</code>
* , but meant to be used out of the box for situations where the UI needs to be exposed through a
* proxy server but for one reason or another the external reverse proxy is not installed or can't
* be configured to perform URL translation on contents.
* </p>
* <p>
* <h2>Init parameters</h2>
* <ul>
* <li><b><code>enabled</code></b>: one of <code>true</code> or <code>false</code>, defaults to
* <code>false</code>. Indicates whether to enable this filter or not.
* <li><b><code>mime-types</code></b>: comma separated list of java regular expressions used to
* match the response mime type and decide whether to perform URL translation on the response
* content or not.
* </ul>
* </p>
* <p>
* <h2>Operation</h2>
* This Filter uses the configured {@link GeoServer#getProxyBaseUrl() proxyBaseUrl} to translate the
* URL's found in textual content whose MIME type matches one of the regular expressions provided
* through the <code>"mime-types"</code> filter init parameter.
* </p>
* <p>
* Sample translations: given GeoServer being running in a servlet engine at
* <code>http://localhost:8080/geoserver</code> and the <code>proxyBaseUrl</code> configured as
* <code>http://myserver/tools/geoserver</code>:
* <ul>
* <li><code>"http://localhost:8080/geoserver/welcome.do"</code> gets translated as
* <code>"http://myserver/tools/geoserver/welcome.do"</code>
* <li><code>"/geoserver/style.css"</code> gets translated as
* <code>"/tools/geoserver/style.css"</code>
* </ul>
* </p>
*
* @author Gabriel Roldan (TOPP)
* @version $Id: ReverseProxyFilter.java 12557 2009-06-04 13:13:19Z groldan $
* @since 2.5.x
* @source $URL:
*         https://svn.codehaus.org/geoserver/trunk/geoserver/web/src/main/java/org/geoserver/filters
*         /ReverseProxyFilter.java $
*/
public class ReverseProxyFilter implements Filter {

    private static final Logger LOGGER = Logging.getLogger("org.geoserver.filters");

    /**
     * Name of the filter init parameter that indicates whether the filter is enabled or disabled
     */
    private static final String ENABLED_INIT_PARAM = "enabled";

    /**
     * The name of the filter init parameter that contains the comma separated list of regular
     * expressions used to match the response mime types to translate URL's for
     */
    private static final String MIME_TYPES_INIT_PARAM = "mime-types";

    private boolean filterIsEnabled;

    /**
     * The set of Patterns used to match response mime types
     */
    private final Set<Pattern> mimeTypePatterns = new HashSet<Pattern>();

    private GeoServerInfo geoServer;

    /**
     * Parses the <code>mime-types</code> init parameter, which is a comma separated list of regular
     * expressions used to match the response mime types to decide whether to apply the URL
     * translation on content or not.
     */
    public void init(final FilterConfig filterConfig) throws ServletException {
        final String enabledInitParam = filterConfig.getInitParameter(ENABLED_INIT_PARAM);

        this.filterIsEnabled = Boolean.valueOf(enabledInitParam).booleanValue();
        if (filterIsEnabled) {
            final String mimeTypesInitParam = filterConfig.getInitParameter(MIME_TYPES_INIT_PARAM);

            GeoServer geoServerConfig = GeoServerExtensions.bean(GeoServer.class);
            if (geoServerConfig == null) {
                throw new ServletException("No " + GeoServer.class.getName()
                        + " found, the system is either not properly "
                        + "configured or the method to get to the GeoServer "
                        + "config instance have changed!");
            }
            geoServer = geoServerConfig.getGlobal();

            if (geoServer == null) {
                throw new ServletException(
                        "No GeoServerInfo instance found. Needed to look for the proxy base URL");
            }
            Set<Pattern> patterns = parsePatterns(geoServer, mimeTypesInitParam);
            this.mimeTypePatterns.addAll(patterns);
            LOGGER.finer("Reverse Proxy Filter configured");
        } else {
            LOGGER.fine("Reverse Proxy Filter is disabled by configuration");
        }
    }

    static Set<Pattern> parsePatterns(final GeoServerInfo geoServer, final String mimeTypesInitParam)
            throws ServletException {

        final String[] split = mimeTypesInitParam.split(",");

        LOGGER.finer("Initializing Reverse Proxy Filter");
        Set<Pattern> mimeTypePatterns = new HashSet<Pattern>();
        try {
            for (int i = 0; i < split.length; i++) {
                String mimeTypeRegExp = split[i];
                LOGGER.finest("Registering mime type regexp for reverse proxy filter: "
                        + mimeTypeRegExp);
                Pattern mimeTypePattern = Pattern.compile(mimeTypeRegExp);
                mimeTypePatterns.add(mimeTypePattern);
            }
        } catch (PatternSyntaxException e) {
            throw new ServletException("Error compiling Reverse Proxy Filter mime-types: "
                    + e.getMessage(), e);
        }
        return mimeTypePatterns;
    }

    /**
     * Uses a response wrapper to evaluate the mime type set and if it matches one of the configured
     * mime types applies URL translation from internal URL's to proxified ones.
     * <p>
     * When a matching mime type is found, the full response is cached during
     * <code>chain.doFilter</code>, and the content is assumed to be textual in the
     * <code>response.getCharacterEncoding()</code> charset. If the mime type does not match any of
     * the configured ones no translation nor response cacheing is performed.
     * <p>
     * </p>
     * The URL translation is a two-step process, done line by line from the cached content and
     * written to the actual response output stream. It first translates the
     * <code>protocol://host:port</code> section of URL's and then replaces the servlet context from
     * the server URL by the proxy base URL context. This accounts for absolute urls as well as
     * relative, root based, urls as used in javascript code and css. </p>
     */
    public void doFilter(final ServletRequest request, final ServletResponse response,
            final FilterChain chain) throws IOException, ServletException {
        LOGGER.finer("filtering " + ((HttpServletRequest) request).getRequestURL());
        if (!filterIsEnabled || !(request instanceof HttpServletRequest)) {
            chain.doFilter(request, response);
            return;
        }

        final String proxyBaseUrl = geoServer.getProxyBaseUrl();

        if (proxyBaseUrl == null || "".equals(proxyBaseUrl)) {
            chain.doFilter(request, response);
            return;
        }

        final CacheingResponseWrapper wrapper = new CacheingResponseWrapper(
                (HttpServletResponse) response, mimeTypePatterns);

        chain.doFilter(request, wrapper);

        wrapper.flushBuffer();

        if (wrapper.isCacheing()) {
            BufferedReader reader;
            {
                byte[] cachedContent = wrapper.getCachedContent();
                String cs = wrapper.getCharacterEncoding();
                reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(
                        cachedContent), cs));
            }
            PrintWriter writer = response.getWriter();
            // the request base url (eg, http://localhost:8080/)
            final String serverBase;
            // the proxy base url (eg, http://myproxyserver/)
            final String proxyBase;
            // the request context (eg, /geoserver/)
            final String context;
            // the proxy context (eg, /tools/geoserver/)
            final String proxyContext;
            final String baseUrl;
            {
                String _baseUrl = RequestUtils.baseURL((HttpServletRequest) request);
                if (_baseUrl.endsWith("/")) {
                    _baseUrl = _baseUrl.substring(0, _baseUrl.length() - 1);
                }
                baseUrl = _baseUrl;
                final URL base = new URL(baseUrl);
                final URL proxy = new URL(proxyBaseUrl);

                serverBase = getServerBase(base);
                proxyBase = getServerBase(proxy);

                context = getContext(base);
                proxyContext = getContext(proxy);
            }

            String line;
            String translatedLine;
            LOGGER.finer("translating " + ((HttpServletRequest) request).getRequestURI());
            while ((line = reader.readLine()) != null) {
                // ugh, we need to revert any already translated URL, like in the case
                // of the server config form where the proxyBaseUrl is set. Otherwise
                // it could be mangled
                if (line.indexOf(proxyBaseUrl) != -1) {
                    translatedLine = line.replaceAll(proxyBaseUrl, baseUrl);
                } else {
                    translatedLine = line;
                }

                // now apply the translation from servlet url to proxy url
                translatedLine = translatedLine.replaceAll(serverBase, proxyBase);
                translatedLine = translatedLine.replaceAll(context, proxyContext);
                if (LOGGER.isLoggable(Level.FINE)) {
                    if (!line.equals(translatedLine)) {
                        LOGGER.finest("translated '" + line + "'");
                        LOGGER.finest("        as '" + translatedLine + "'");
                    }
                }
                writer.println(translatedLine);
            }
            writer.flush();
        }
    }

    private String getContext(URL url) {
        String context = url.getPath();
        return context.endsWith("/") ? context : context + "/";
    }

    private String getServerBase(URL url) {
        StringBuffer sb = new StringBuffer();
        sb.append(url.getProtocol()).append("://");
        sb.append(url.getHost());
        if (url.getPort() != -1) {
            sb.append(":").append(url.getPort());
        }
        sb.append("/");
        return sb.toString();
    }

    public void destroy() {
    }

    /**
     * A servlet response wrapper that caches the content if its mime type matches one of the
     * provided patterns.
     * <p>
     * Whether to cache the content or not has to be decided when {@link #setContentType(String)} is
     * called, doing the pattern matching with the provided set of regular expression patterns. So
     * after using this response wrapper, {@link #isCacheing()} indicates whether content cache was
     * done, and if so, the cached content is accessed through {@link #getCachedContent()}.
     * </p>
     *
     * @author Gabriel Roldan (TOPP)
     * @version $Id: ReverseProxyFilter.java 12557 2009-06-04 13:13:19Z groldan $
     * @since 2.5.x
     * @source $URL:
     *         https://svn.codehaus.org/geoserver/trunk/geoserver/web/src/main/java/org/geoserver
     *         /filters/ReverseProxyFilter.java $
     */
    private static class CacheingResponseWrapper extends HttpServletResponseWrapper {

        private Set<Pattern> cacheingMimeTypes;

        private boolean cacheContent;

        private DeferredCacheingOutputStream outputStream;

        private PrintWriter writer;

        /**
         * @param response
         *            the wrapped response
         * @param cacheingMimeTypes
         *            the patterns to do mime type matching with to decide whether to cache content
         *            or not
         */
        public CacheingResponseWrapper(final HttpServletResponse response,
                Set<Pattern> cacheingMimeTypes) {
            super(response);
            this.cacheingMimeTypes = cacheingMimeTypes;
            // we can't know until setContentType is called
            this.cacheContent = false;
        }

        /**
         * @return whether content cacheing has been accomplished or not after the response was
         *         used.
         */
        public boolean isCacheing() {
            return cacheContent;
        }

        /**
         * @return the cached contend, as long as <code>isCacheing() == true</code>
         */
        public byte[] getCachedContent() {
            return outputStream.getCachedContent();
        }

        /**
         * Among setting the response content type, determines whether the response content should
         * be cached or not, depending on the <code>mimeType</code> matching one of the patterns or
         * not.
         */
        @Override
        public void setContentType(final String mimeType) {
            Pattern p;
            for (Iterator<Pattern> it = cacheingMimeTypes.iterator(); it.hasNext();) {
                p = it.next();
                Matcher matcher = p.matcher(mimeType);
                if (matcher.matches()) {
                    cacheContent = true;
                    break;
                }
            }
            super.setContentType(mimeType);
        }

        @Override
        public void flushBuffer() throws IOException {
            if (cacheContent) {
                if (writer != null) {
                    writer.flush();
                }
                if (outputStream != null) {
                    outputStream.flush();
                }
            } else {
                super.flushBuffer();
            }
        }

        /**
         * Waits until the first write operation to decide whether to cache the contents or not.
         * This way it tolerates calls to {@link ServletResponse#getOutputStream()} before calling
         * {@link ServletResponse#setContentType(String)}.
         *
         * @author Gabriel Roldan
         */
        private class DeferredCacheingOutputStream extends ServletOutputStream {
            /**
             * non null iif {@link CacheingResponseWrapper#isCacheing()} == false
             */
            private ServletOutputStream actualStream;

            /**
             * non null iif {@link CacheingResponseWrapper#isCacheing()} == true
             */
            private ByteArrayOutputStream cache;

            @Override
            public void write(int b) throws IOException {
                if (isCacheing()) {
                    if (cache == null) {
                        cache = new ByteArrayOutputStream();
                    }
                    cache.write(b);
                } else {
                    if (actualStream == null) {
                        actualStream = getOutputStreamInternal();
                    }
                    actualStream.write(b);
                }
            }

            public byte[] getCachedContent() {
                if (cache == null) {
                    // the request produced no content
                    return new byte[0];
                }
                return cache.toByteArray();
            }

            @Override
            public void flush() throws IOException {
                if (actualStream != null) {
                    actualStream.flush();
                }
            }
        }

        @Override
        public ServletOutputStream getOutputStream() throws IOException {
            if (outputStream == null) {
                outputStream = new DeferredCacheingOutputStream();
            }
            return outputStream;
        }

        private ServletOutputStream getOutputStreamInternal() throws IOException {
            return super.getOutputStream();
        }

        /**
         * The default behavior of this method is to return getWriter() on the wrapped response
         * object.
         */
        @Override
        public PrintWriter getWriter() throws IOException {
            if (writer == null) {
                if (cacheContent) {
                    String charset = super.getCharacterEncoding();
                    writer = new PrintWriter(new OutputStreamWriter(getOutputStream(), charset));
                } else {
                    writer = super.getWriter();
                }
            }
            return writer;
        }
    }
}
TOP

Related Classes of org.geoserver.filters.ReverseProxyFilter$CacheingResponseWrapper

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.