[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index] [List Home]
Re: [jetty-dev] ProxyServlet customizeExchange : add parameter response ?

Hi Jesse,

Attached is the patch and an example usage. It's not finished yet but it will look like this.

If you're interested in having this jettty-servlets, tell me. Of course I'll add tests cases.

If you have any comments on the solution, I would appreciate.

Thanks

Le 01/03/2012 20:23, Jesse McConnell a Ãcrit :
I am interested to see what your doing and so long as its not going to
fundamentally break existing usage it would be nice to see the patch
for it.

where possible respect the existing api :)

cheers,
jesse

--
jesse mcconnell
jesse.mcconnell@xxxxxxxxx



On Thu, Mar 1, 2012 at 11:54, Thomas SEGISMONT<tsegismont@xxxxxxxxx> wrote:
Going further on my prototype, I realize that in order to add other
functionalities (back-end balancing, sticky sessions, ...) I'll need more
customizations than I pushed in this change set :
https://git.eclipse.org/r/#/c/5211/

So I have set it abandoned in Gerrit.

Are you interested in having such functionalities in ProxyServlet ? If so i
can push a more complete change set later.

Otherwise I'll make my own servlet (without taking into account backward
compatibility issues).

Cheers

Le 01/03/2012 13:43, Thomas SEGISMONT a Ãcrit :

Hi,

Thanks for the quick answer and sorry for waking you up ;)

I'll push a changeset to gerrit. Which branch should I base the change on
?

Le 01/03/2012 12:48, Jesse McConnell a Ãcrit :
we will certainly take a look, need a coffee before I consider it
right or not :)

feel free to open a bugzilla and attached the patch or push the patch
into gerrit for review

https://git.eclipse.org/r/#/q/project:jetty/org.eclipse.jetty.project,n,z

cheers,
jesse

--
jesse mcconnell
jesse.mcconnell@xxxxxxxxx



On Thu, Mar 1, 2012 at 05:02, Thomas SEGISMONT<tsegismont@xxxxxxxxx>
  wrote:
Hi,

I'm trying to build a http transparent proxy, similar to what you get
with
Apache mod_proxy, ProxyPass and ProxyPassReverse. So I need to intercept
response headers sent by the back-end server ( Location,
Content-Location
and URI).

I have a servlet which extends ProxyServlet.Transparent and overrides
method
customizeExchange :

@Override
protected void customizeExchange(HttpExchange exchange,
HttpServletRequest
request)
{
    exchange.setEventListener(new
ProxyEventListener(exchange.getEventListener(), request));
}

Here I miss a reference to the HttpServletResponse instance. I'd like to
have :

@Override
protected void customizeExchange(HttpExchange exchange,
HttpServletRequest
request, HttpServletResponse response)
{
    exchange.setEventListener(new
ProxyEventListener(exchange.getEventListener(), request, response));
}

Would you accept a patch like this ? Or do you think there's another way
to
do the job ?

Thanks
_______________________________________________
jetty-dev mailing list
jetty-dev@xxxxxxxxxxx
https://dev.eclipse.org/mailman/listinfo/jetty-dev
_______________________________________________
jetty-dev mailing list
jetty-dev@xxxxxxxxxxx
https://dev.eclipse.org/mailman/listinfo/jetty-dev

_______________________________________________
jetty-dev mailing list
jetty-dev@xxxxxxxxxxx
https://dev.eclipse.org/mailman/listinfo/jetty-dev
_______________________________________________
jetty-dev mailing list
jetty-dev@xxxxxxxxxxx
https://dev.eclipse.org/mailman/listinfo/jetty-dev

diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/BalancerServlet.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/BalancerServlet.java
new file mode 100644
index 0000000..cfd9639
--- /dev/null
+++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/BalancerServlet.java
@@ -0,0 +1,379 @@
+// ========================================================================
+// Copyright (c) 2009-2009 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.servlets;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.UnavailableException;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jetty.http.HttpURI;
+
+/**
+ * 
+ */
+public final class BalancerServlet extends ProxyServlet
+{
+
+    private static final class BalancerMember
+    {
+
+        private String _name;
+
+        private String _proxyTo;
+
+        public BalancerMember(String name, String proxyTo)
+        {
+            super();
+            _name = name;
+            _proxyTo = proxyTo;
+        }
+
+        public String getName()
+        {
+            return _name;
+        }
+
+        public String getProxyTo()
+        {
+            return _proxyTo;
+        }
+
+        @Override
+        public String toString()
+        {
+            return "BalancerMember [_name=" + _name + ", _proxyTo=" + _proxyTo + "]";
+        }
+
+        @Override
+        public int hashCode()
+        {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + ((_name == null)?0:_name.hashCode());
+            return result;
+        }
+
+        @Override
+        public boolean equals(Object obj)
+        {
+            if (this == obj)
+                return true;
+            if (obj == null)
+                return false;
+            if (getClass() != obj.getClass())
+                return false;
+            BalancerMember other = (BalancerMember)obj;
+            if (_name == null)
+            {
+                if (other._name != null)
+                    return false;
+            }
+            else if (!_name.equals(other._name))
+                return false;
+            return true;
+        }
+
+    }
+
+    private static final class RoundRobinIterator implements Iterator<BalancerMember>
+    {
+
+        private BalancerMember[] _balancerMembers;
+
+        private AtomicInteger _index;
+
+        public RoundRobinIterator(Collection<BalancerMember> balancerMembers)
+        {
+            _balancerMembers = (BalancerMember[])balancerMembers.toArray(new BalancerMember[balancerMembers.size()]);
+            _index = new AtomicInteger(-1);
+        }
+
+        public boolean hasNext()
+        {
+            return true;
+        }
+
+        public BalancerMember next()
+        {
+            BalancerMember balancerMember = null;
+            while (balancerMember == null)
+            {
+                int currentIndex = _index.get();
+                int nextIndex = (currentIndex + 1) % _balancerMembers.length;
+                if (_index.compareAndSet(currentIndex,nextIndex))
+                {
+                    balancerMember = _balancerMembers[nextIndex];
+                }
+            }
+            return balancerMember;
+        }
+
+        public void remove()
+        {
+            throw new UnsupportedOperationException();
+        }
+
+    }
+
+    private static final String BALANCER_MEMBER_PREFIX = "BalancerMember.";
+
+    private static final List<String> FORBIDDEN_CONFIG_PARAMETERS;
+    static
+    {
+        List<String> params = new LinkedList<String>();
+        params.add("HostHeader");
+        params.add("whiteList");
+        params.add("blackList");
+        FORBIDDEN_CONFIG_PARAMETERS = Collections.unmodifiableList(params);
+    }
+
+    private static final List<String> REVERSE_PROXY_HEADERS;
+    static
+    {
+        List<String> params = new LinkedList<String>();
+        params.add("Location");
+        params.add("Content-Location");
+        params.add("URI");
+        REVERSE_PROXY_HEADERS = Collections.unmodifiableList(params);
+    }
+
+    private String _prefix;
+
+    private boolean _stickySessions;
+
+    private Set<BalancerMember> _balancerMembers = new HashSet<BalancerMember>();
+
+    private RoundRobinIterator _roundRobinIterator;
+
+    @Override
+    public void init(ServletConfig config) throws ServletException
+    {
+        validateConfig(config);
+        super.init(config);
+        initPrefix(config);
+        initStickySessions(config);
+        initBalancers(config);
+        postInit();
+    }
+
+    private void validateConfig(ServletConfig config) throws ServletException
+    {
+        @SuppressWarnings("unchecked")
+        List<String> initParameterNames = Collections.list(config.getInitParameterNames());
+        for (String initParameterName : initParameterNames)
+        {
+            if (FORBIDDEN_CONFIG_PARAMETERS.contains(initParameterName))
+            {
+                throw new UnavailableException(initParameterName + " not supported in " + getClass().getName());
+            }
+        }
+    }
+
+    private void initPrefix(ServletConfig config) throws ServletException
+    {
+        _prefix = config.getInitParameter("Prefix");
+        // Adjust prefix value to account for context path
+        String contextPath = _context.getContextPath();
+        _prefix = _prefix == null?contextPath:(contextPath + _prefix);
+        if (!_prefix.startsWith("/"))
+            throw new UnavailableException("Prefix parameter must start with a '/'.");
+    }
+
+    private void initStickySessions(ServletConfig config) throws ServletException
+    {
+        _stickySessions = "true".equalsIgnoreCase(config.getInitParameter("StickySessions"));
+    }
+
+    private void initBalancers(ServletConfig config) throws ServletException
+    {
+        Set<String> balancerNames = getBalancerNames(config);
+        for (String balancerName : balancerNames)
+        {
+            String memberProxyToParam = BALANCER_MEMBER_PREFIX + balancerName + ".ProxyTo";
+            String proxyTo = config.getInitParameter(memberProxyToParam);
+            if (proxyTo == null || proxyTo.trim().length() == 0)
+            {
+                throw new UnavailableException(memberProxyToParam + " parameter is empty.");
+            }
+            _balancerMembers.add(new BalancerMember(balancerName,proxyTo));
+        }
+    }
+
+    private void postInit()
+    {
+        _roundRobinIterator = new RoundRobinIterator(_balancerMembers);
+    }
+
+    private Set<String> getBalancerNames(ServletConfig config) throws ServletException
+    {
+        Set<String> names = new HashSet<String>();
+        @SuppressWarnings("unchecked")
+        List<String> initParameterNames = Collections.list(config.getInitParameterNames());
+        for (String initParameterName : initParameterNames)
+        {
+            if (!initParameterName.startsWith(BALANCER_MEMBER_PREFIX))
+            {
+                continue;
+            }
+            int endOfNameIndex = initParameterName.lastIndexOf(".");
+            if (endOfNameIndex <= BALANCER_MEMBER_PREFIX.length())
+            {
+                throw new UnavailableException(initParameterName + " parameter does not provide a balancer member name");
+            }
+            names.add(initParameterName.substring(BALANCER_MEMBER_PREFIX.length(),endOfNameIndex));
+        }
+        return names;
+    }
+
+    @Override
+    protected HttpURI proxyHttpURI(String scheme, String serverName, int serverPort, String uri, HttpServletRequest request) throws MalformedURLException
+    {
+        if (!uri.startsWith(_prefix))
+            return null;
+        BalancerMember balancerMember = selectBalancerMember(request);
+        try
+        {
+            URI dstUri = new URI(balancerMember.getProxyTo() + uri.substring(_prefix.length())).normalize();
+            return new HttpURI(dstUri.toString());
+        }
+        catch (URISyntaxException e)
+        {
+            throw new MalformedURLException(e.getMessage());
+        }
+    }
+
+    private BalancerMember selectBalancerMember(HttpServletRequest request)
+    {
+        BalancerMember balancerMember = null;
+        if (_stickySessions)
+        {
+            String name = getBalancerMemberNameFromSessionId(request);
+            if (name != null)
+            {
+                balancerMember = findBalancerMemberByName(name);
+                if (balancerMember != null)
+                {
+                    return balancerMember;
+                }
+            }
+        }
+        return _roundRobinIterator.next();
+    }
+
+    private BalancerMember findBalancerMemberByName(String name)
+    {
+        BalancerMember example = new BalancerMember(name,"");
+        for (BalancerMember balancerMember : _balancerMembers)
+        {
+            if (balancerMember.equals(example))
+            {
+                return balancerMember;
+            }
+        }
+        return null;
+    }
+
+    private String getBalancerMemberNameFromSessionId(HttpServletRequest request)
+    {
+        String name = getBalancerMemberNameFromSessionCookie(request);
+        if (name == null)
+        {
+            name = getBalancerMemberNameFromURL(request);
+        }
+        return name;
+    }
+
+    private String getBalancerMemberNameFromSessionCookie(HttpServletRequest request)
+    {
+        Cookie[] cookies = request.getCookies();
+        String name = null;
+        for (Cookie cookie : cookies)
+        {
+            if ("jsessionid".equalsIgnoreCase(cookie.getName()))
+            {
+                name = extractBalancerMemberNameFromSessionId(cookie.getValue());
+                break;
+            }
+        }
+        return name;
+    }
+
+    private String getBalancerMemberNameFromURL(HttpServletRequest request)
+    {
+        String name = null;
+        String requestURI = request.getRequestURI();
+        int idx = requestURI.lastIndexOf(";");
+        if (idx != -1)
+        {
+            String requestURISuffix = requestURI.substring(idx);
+            if (requestURISuffix.startsWith("jsessionid="))
+            {
+                name = extractBalancerMemberNameFromSessionId(requestURISuffix.substring("jsessionid=".length()));
+            }
+        }
+        return name;
+    }
+
+    private String extractBalancerMemberNameFromSessionId(String sessionId)
+    {
+        String name = null;
+        int idx = sessionId.lastIndexOf(".");
+        if (idx != -1)
+        {
+            String sessionIdSuffix = sessionId.substring(idx + 1);
+            name = (sessionIdSuffix.length() > 0)?sessionIdSuffix:null;
+        }
+        return name;
+    }
+
+    @Override
+    protected String filterResponseHeaderValue(String headerName, String headerValue, HttpServletRequest request)
+    {
+        // TODO : filter for reverse proxy
+        return headerValue;
+    }
+
+    @Override
+    public String getHostHeader()
+    {
+        throw new UnsupportedOperationException("HostHeader not supported in " + getClass().getName());
+    }
+
+    @Override
+    public void setHostHeader(String hostHeader)
+    {
+        throw new UnsupportedOperationException("HostHeader not supported in " + getClass().getName());
+    }
+
+    @Override
+    public boolean validateDestination(String host, String path)
+    {
+        return true;
+    }
+
+}
diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ProxyServlet.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ProxyServlet.java
index 445a1bf..ff9383b 100644
--- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ProxyServlet.java
+++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ProxyServlet.java
@@ -314,10 +314,11 @@ public class ProxyServlet implements Servlet
             Object whiteObj = _white.getLazyMatches(host);
             if (whiteObj != null)
             {
-                List whiteList = (whiteObj instanceof List)?(List)whiteObj:Collections.singletonList(whiteObj);
+                List<?> whiteList = (whiteObj instanceof List)?(List<?>)whiteObj:Collections.singletonList(whiteObj);
 
                 for (Object entry : whiteList)
                 {
+                    @SuppressWarnings("unchecked")
                     PathMap pathMap = ((Map.Entry<String, PathMap>)entry).getValue();
                     if (match = (pathMap != null && (pathMap.size() == 0 || pathMap.match(path) != null)))
                         break;
@@ -333,10 +334,11 @@ public class ProxyServlet implements Servlet
             Object blackObj = _black.getLazyMatches(host);
             if (blackObj != null)
             {
-                List blackList = (blackObj instanceof List)?(List)blackObj:Collections.singletonList(blackObj);
+                List<?> blackList = (blackObj instanceof List)?(List<?>)blackObj:Collections.singletonList(blackObj);
 
                 for (Object entry : blackList)
                 {
+                    @SuppressWarnings("unchecked")
                     PathMap pathMap = ((Map.Entry<String, PathMap>)entry).getValue();
                     if (pathMap != null && (pathMap.size() == 0 || pathMap.match(path) != null))
                         return false;
@@ -414,7 +416,7 @@ public class ProxyServlet implements Servlet
                 if (request.getQueryString() != null)
                     uri += "?" + request.getQueryString();
 
-                HttpURI url = proxyHttpURI(request.getScheme(),request.getServerName(),request.getServerPort(),uri);
+                HttpURI url = proxyHttpURI(request.getScheme(),request.getServerName(),request.getServerPort(),uri,request);
 
                 if (debug != 0)
                     _log.debug(debug + " proxy " + uri + "-->" + url);
@@ -473,13 +475,18 @@ public class ProxyServlet implements Servlet
                     @Override
                     protected void onResponseHeader(Buffer name, Buffer value) throws IOException
                     {
-                        String s = name.toString().toLowerCase();
+                        String nameString = name.toString();
+                        String s = nameString.toLowerCase();
                         if (!_DontProxyHeaders.contains(s) || (HttpHeaders.CONNECTION_BUFFER.equals(name) && HttpHeaderValues.CLOSE_BUFFER.equals(value)))
                         {
                             if (debug != 0)
                                 _log.debug(debug + " " + name + ": " + value);
 
-                            response.addHeader(name.toString(),value.toString());
+                            String filteredHeaderValue = filterResponseHeaderValue(nameString,value.toString(),request);
+                            if (filteredHeaderValue != null && filteredHeaderValue.trim().length() > 0)
+                            {
+                                response.addHeader(nameString,filteredHeaderValue);
+                            }
                         }
                         else if (debug != 0)
                             _log.debug(debug + " " + name + "! " + value);
@@ -492,10 +499,11 @@ public class ProxyServlet implements Servlet
 
                         // it is possible this might trigger before the
                         // continuation.suspend()
-                        if (!continuation.isInitial())
+                        if (continuation.isInitial())
                         {
-                            continuation.complete();
+                            continuation.suspend(response);
                         }
+                        continuation.complete();
                     }
 
                     @Override
@@ -510,10 +518,11 @@ public class ProxyServlet implements Servlet
 
                         // it is possible this might trigger before the
                         // continuation.suspend()
-                        if (!continuation.isInitial())
+                        if (continuation.isInitial())
                         {
-                            continuation.complete();
+                            continuation.suspend(response);
                         }
+                        continuation.complete();
                     }
 
                     @Override
@@ -677,6 +686,19 @@ public class ProxyServlet implements Servlet
     }
 
     /* ------------------------------------------------------------ */
+
+    /* ------------------------------------------------------------ */
+
+    /**
+     * @param scheme
+     * @param serverName
+     * @param serverPort
+     * @param uri
+     * @return
+     * @throws MalformedURLException
+     * @Deprecated subclasses of {@link ProxyServlet} should override {@link #proxyHttpURI(String, String, int, String, HttpServletRequest)}
+     */
+    @Deprecated
     protected HttpURI proxyHttpURI(String scheme, String serverName, int serverPort, String uri) throws MalformedURLException
     {
         if (!validateDestination(serverName,uri))
@@ -685,6 +707,13 @@ public class ProxyServlet implements Servlet
         return new HttpURI(scheme + "://" + serverName + ":" + serverPort + uri);
     }
 
+
+    protected HttpURI proxyHttpURI(String scheme, String serverName, int serverPort, String uri, HttpServletRequest request)
+            throws MalformedURLException
+    {
+        return this.proxyHttpURI(scheme,serverName,serverPort,uri);
+    }
+
     /*
      * (non-Javadoc)
      *
@@ -771,8 +800,22 @@ public class ProxyServlet implements Servlet
     }
 
     /**
+     * Extension point for remote server response header filtering. The default implementation returns the header value as is. If null is returned, this header
+     * won't be forwarded back to the client.
+     * 
+     * @param headerName
+     * @param headerValue
+     * @param request
+     * @return filteredHeaderValue
+     */
+    protected String filterResponseHeaderValue(String headerName, String headerValue, HttpServletRequest request)
+    {
+        return headerValue;
+    }
+
+    /**
      * Transparent Proxy.
-     *
+     * 
      * This convenience extension to ProxyServlet configures the servlet as a transparent proxy. The servlet is configured with init parameters:
      * <ul>
      * <li>ProxyTo - a URI like http://host:80/context to which the request is proxied.
@@ -780,7 +823,7 @@ public class ProxyServlet implements Servlet
      * </ul>
      * For example, if a request was received at /foo/bar and the ProxyTo was http://host:80/context and the Prefix was /foo, then the request would be proxied
      * to http://host:80/context/bar
-     *
+     * 
      */
     public static class Transparent extends ProxyServlet
     {
package org.example;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.servlets.BalancerServlet;

public class BalancerServletExample
{

    public static void main(String[] args) throws Exception
    {
        Server server = new Server(9090);

        ServletContextHandler context = new ServletContextHandler();
        context.setContextPath("/");
        server.setHandler(context);

        BalancerServlet balancerServlet = new BalancerServlet();
        ServletHolder servletHolder = new ServletHolder(balancerServlet);
        servletHolder.setInitParameter("Prefix", "/mycontext");
        servletHolder.setInitParameter("StickySessions", "true");
        servletHolder.setInitParameter("BalancerMember.node1.ProxyTo", "http://localhost:8080/mycontext";);
        servletHolder.setInitParameter("BalancerMember.node2.ProxyTo", "http://localhost:9080/mycontext";);

        context.addServlet(servletHolder, "/mycontext/*");

        server.start();

        server.join();
    }

}