From 226c5224519a0dc715ef2189e14ce989fcc9fa2e Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Thu, 31 Oct 2013 19:54:59 +0100 Subject: [PATCH] Implemented a TryFilesFilter inspired by nginx's try_files directive. This is needed by - for example - WordPress because it allows for different permalinks schemes. For example, /foo could be a permalink that should be directed to PHP, while /favicon.ico is a file that must be served statically. URI /foo would be matched by the default servlet resulting in a 404. Therefore we need a filter mapped to /* that checks whether the request URI is a file that exists and likely to be served by the default servlet, otherwise it needs to rewrite the URL to something like /index.php?p=/foo. The rewrite filter does not have the functionality of "try the file, if missing then rewrite" that is now implemented by the TryFilesFilter. --- .../jetty/fcgi/proxy/TryFilesFilter.java | 109 ++++++++++++++++++ .../WordPressSPDYFastCGIProxyServer.java | 86 ++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 fcgi-proxy/src/main/java/org/eclipse/jetty/fcgi/proxy/TryFilesFilter.java create mode 100644 fcgi-proxy/src/test/java/org/eclipse/jetty/fcgi/proxy/WordPressSPDYFastCGIProxyServer.java diff --git a/fcgi-proxy/src/main/java/org/eclipse/jetty/fcgi/proxy/TryFilesFilter.java b/fcgi-proxy/src/main/java/org/eclipse/jetty/fcgi/proxy/TryFilesFilter.java new file mode 100644 index 00000000000..1208889e7bb --- /dev/null +++ b/fcgi-proxy/src/main/java/org/eclipse/jetty/fcgi/proxy/TryFilesFilter.java @@ -0,0 +1,109 @@ +// +// ======================================================================== +// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.fcgi.proxy; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Inspired by nginx's try_files functionality + */ +public class TryFilesFilter implements Filter +{ + public static final String ROOT_INIT_PARAM = "root"; + public static final String FILES_INIT_PARAM = "files"; + + private String root; + private String[] files; + + @Override + public void init(FilterConfig config) throws ServletException + { + root = config.getInitParameter(ROOT_INIT_PARAM); + if (root == null) + throw new ServletException(String.format("Missing mandatory parameter '%s'", ROOT_INIT_PARAM)); + String param = config.getInitParameter(FILES_INIT_PARAM); + if (param == null) + throw new ServletException(String.format("Missing mandatory parameter '%s'", FILES_INIT_PARAM)); + files = param.split(" "); + } + + public String getRoot() + { + return root; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException + { + HttpServletRequest httpRequest = (HttpServletRequest)request; + HttpServletResponse httpResponse = (HttpServletResponse)response; + + for (int i = 0; i < files.length - 1; ++i) + { + String file = files[i]; + String resolved = resolve(httpRequest, file); + Path path = Paths.get(getRoot(), resolved); + if (Files.exists(path) && Files.isReadable(path)) + { + chain.doFilter(httpRequest, httpResponse); + return; + } + } + + // The last one is the fallback + fallback(httpRequest, httpResponse, chain, files[files.length - 1]); + } + + protected void fallback(HttpServletRequest request, HttpServletResponse response, FilterChain chain, String fallback) throws IOException, ServletException + { + String resolved = resolve(request, fallback); + request.getRequestDispatcher(resolved).forward(request, response); + } + + private String resolve(HttpServletRequest request, String value) + { + String path = request.getRequestURI(); + String query = request.getQueryString(); + + String result = value.replaceAll("\\$path", path); + result = result.replaceAll("\\$query", query == null ? "" : query); + + // Remove the "?" or "&" at the end if there is no query + if (query == null && (result.endsWith("?") || result.endsWith("&"))) + result = result.substring(0, result.length() - 1); + + return result; + } + + @Override + public void destroy() + { + } +} diff --git a/fcgi-proxy/src/test/java/org/eclipse/jetty/fcgi/proxy/WordPressSPDYFastCGIProxyServer.java b/fcgi-proxy/src/test/java/org/eclipse/jetty/fcgi/proxy/WordPressSPDYFastCGIProxyServer.java new file mode 100644 index 00000000000..3872536368f --- /dev/null +++ b/fcgi-proxy/src/test/java/org/eclipse/jetty/fcgi/proxy/WordPressSPDYFastCGIProxyServer.java @@ -0,0 +1,86 @@ +// +// ======================================================================== +// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.fcgi.proxy; + +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.DispatcherType; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.servlet.FilterHolder; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.spdy.api.SPDY; +import org.eclipse.jetty.spdy.server.http.HTTPSPDYServerConnector; +import org.eclipse.jetty.spdy.server.http.PushStrategy; +import org.eclipse.jetty.spdy.server.http.ReferrerPushStrategy; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +public class WordPressSPDYFastCGIProxyServer +{ + public static void main(String[] args) throws Exception + { +// int port = 8080; + int port = 8443; + +// SslContextFactory sslContextFactory = null; + SslContextFactory sslContextFactory = new SslContextFactory(); + sslContextFactory.setEndpointIdentificationAlgorithm(""); + sslContextFactory.setKeyStorePath("src/test/resources/keystore.jks"); + sslContextFactory.setKeyStorePassword("storepwd"); + sslContextFactory.setTrustStorePath("src/test/resources/truststore.jks"); + sslContextFactory.setTrustStorePassword("storepwd"); + + Server server = new Server(); + + Map pushStrategies = new HashMap<>(); + pushStrategies.put(SPDY.V3, new ReferrerPushStrategy()); + HTTPSPDYServerConnector connector = new HTTPSPDYServerConnector(server, sslContextFactory, pushStrategies); + connector.setPort(port); + server.addConnector(connector); + + String root = "/home/simon/programs/wordpress-3.7.1"; + + ServletContextHandler context = new ServletContextHandler(server, "/"); + context.setResourceBase(root); + context.setWelcomeFiles(new String[]{"index.php"}); + + // Serve static resources + ServletHolder defaultServlet = new ServletHolder(DefaultServlet.class); + defaultServlet.setName("default"); + context.addServlet(defaultServlet, "/"); + + FilterHolder tryFileFilter = context.addFilter(TryFilesFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); + tryFileFilter.setInitParameter(TryFilesFilter.ROOT_INIT_PARAM, root); +// tryFileFilter.setInitParameter(TryFilesFilter.FILES_INIT_PARAM, "$path $path/index.php?$query"); // Permalink /?p=123 + tryFileFilter.setInitParameter(TryFilesFilter.FILES_INIT_PARAM, "$path /index.php?p=$path&$query"); // Permalink /%year%/%monthnum%/%postname% + + // FastCGI + ServletHolder fcgiServlet = new ServletHolder(FastCGIProxyServlet.class); + fcgiServlet.setInitParameter(FastCGIProxyServlet.SCRIPT_ROOT_INIT_PARAM, root); + fcgiServlet.setInitParameter("proxyTo", "http://localhost:9000"); + fcgiServlet.setInitParameter("prefix", "/"); + fcgiServlet.setInitParameter(FastCGIProxyServlet.SCRIPT_PATTERN_INIT_PARAM, "(.+?\\.php)"); + context.addServlet(fcgiServlet, "*.php"); + + server.start(); + } +}