View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.proxy;
20  
21  import java.net.URI;
22  import java.util.ArrayList;
23  import java.util.Collections;
24  import java.util.HashSet;
25  import java.util.LinkedList;
26  import java.util.List;
27  import java.util.Set;
28  import java.util.concurrent.atomic.AtomicLong;
29  
30  import javax.servlet.ServletException;
31  import javax.servlet.UnavailableException;
32  import javax.servlet.http.Cookie;
33  import javax.servlet.http.HttpServletRequest;
34  
35  public class BalancerServlet extends ProxyServlet
36  {
37      private static final String BALANCER_MEMBER_PREFIX = "balancerMember.";
38      private static final List<String> FORBIDDEN_CONFIG_PARAMETERS;
39  
40      static
41      {
42          List<String> params = new LinkedList<>();
43          params.add("hostHeader");
44          params.add("whiteList");
45          params.add("blackList");
46          FORBIDDEN_CONFIG_PARAMETERS = Collections.unmodifiableList(params);
47      }
48  
49      private static final List<String> REVERSE_PROXY_HEADERS;
50  
51      static
52      {
53          List<String> params = new LinkedList<>();
54          params.add("Location");
55          params.add("Content-Location");
56          params.add("URI");
57          REVERSE_PROXY_HEADERS = Collections.unmodifiableList(params);
58      }
59  
60      private static final String JSESSIONID = "jsessionid";
61      private static final String JSESSIONID_URL_PREFIX = JSESSIONID + "=";
62  
63      private final List<BalancerMember> _balancerMembers = new ArrayList<>();
64      private final AtomicLong counter = new AtomicLong();
65      private boolean _stickySessions;
66      private boolean _proxyPassReverse;
67  
68      @Override
69      public void init() throws ServletException
70      {
71          validateConfig();
72          super.init();
73          initStickySessions();
74          initBalancers();
75          initProxyPassReverse();
76      }
77  
78      private void validateConfig() throws ServletException
79      {
80          for (String initParameterName : Collections.list(getServletConfig().getInitParameterNames()))
81          {
82              if (FORBIDDEN_CONFIG_PARAMETERS.contains(initParameterName))
83              {
84                  throw new UnavailableException(initParameterName + " not supported in " + getClass().getName());
85              }
86          }
87      }
88  
89      private void initStickySessions() throws ServletException
90      {
91          _stickySessions = Boolean.parseBoolean(getServletConfig().getInitParameter("stickySessions"));
92      }
93  
94      private void initBalancers() throws ServletException
95      {
96          Set<BalancerMember> members = new HashSet<>();
97          for (String balancerName : getBalancerNames())
98          {
99              String memberProxyToParam = BALANCER_MEMBER_PREFIX + balancerName + ".proxyTo";
100             String proxyTo = getServletConfig().getInitParameter(memberProxyToParam);
101             if (proxyTo == null || proxyTo.trim().length() == 0)
102                 throw new UnavailableException(memberProxyToParam + " parameter is empty.");
103             members.add(new BalancerMember(balancerName, proxyTo));
104         }
105         _balancerMembers.addAll(members);
106     }
107 
108     private void initProxyPassReverse()
109     {
110         _proxyPassReverse = Boolean.parseBoolean(getServletConfig().getInitParameter("proxyPassReverse"));
111     }
112 
113     private Set<String> getBalancerNames() throws ServletException
114     {
115         Set<String> names = new HashSet<>();
116         for (String initParameterName : Collections.list(getServletConfig().getInitParameterNames()))
117         {
118             if (!initParameterName.startsWith(BALANCER_MEMBER_PREFIX))
119                 continue;
120 
121             int endOfNameIndex = initParameterName.lastIndexOf(".");
122             if (endOfNameIndex <= BALANCER_MEMBER_PREFIX.length())
123                 throw new UnavailableException(initParameterName + " parameter does not provide a balancer member name");
124 
125             names.add(initParameterName.substring(BALANCER_MEMBER_PREFIX.length(), endOfNameIndex));
126         }
127         return names;
128     }
129 
130     @Override
131     protected URI rewriteURI(HttpServletRequest request)
132     {
133         BalancerMember balancerMember = selectBalancerMember(request);
134         _log.debug("Selected {}", balancerMember);
135         String path = request.getRequestURI();
136         String query = request.getQueryString();
137         if (query != null)
138             path += "?" + query;
139         return URI.create(balancerMember.getProxyTo() + "/" + path).normalize();
140     }
141 
142     private BalancerMember selectBalancerMember(HttpServletRequest request)
143     {
144         if (_stickySessions)
145         {
146             String name = getBalancerMemberNameFromSessionId(request);
147             if (name != null)
148             {
149                 BalancerMember balancerMember = findBalancerMemberByName(name);
150                 if (balancerMember != null)
151                     return balancerMember;
152             }
153         }
154         int index = (int)(counter.getAndIncrement() % _balancerMembers.size());
155         return _balancerMembers.get(index);
156     }
157 
158     private BalancerMember findBalancerMemberByName(String name)
159     {
160         for (BalancerMember balancerMember : _balancerMembers)
161         {
162             if (balancerMember.getName().equals(name))
163                 return balancerMember;
164         }
165         return null;
166     }
167 
168     private String getBalancerMemberNameFromSessionId(HttpServletRequest request)
169     {
170         String name = getBalancerMemberNameFromSessionCookie(request);
171         if (name == null)
172             name = getBalancerMemberNameFromURL(request);
173         return name;
174     }
175 
176     private String getBalancerMemberNameFromSessionCookie(HttpServletRequest request)
177     {
178         for (Cookie cookie : request.getCookies())
179         {
180             if (JSESSIONID.equalsIgnoreCase(cookie.getName()))
181                 return extractBalancerMemberNameFromSessionId(cookie.getValue());
182         }
183         return null;
184     }
185 
186     private String getBalancerMemberNameFromURL(HttpServletRequest request)
187     {
188         String requestURI = request.getRequestURI();
189         int idx = requestURI.lastIndexOf(";");
190         if (idx > 0)
191         {
192             String requestURISuffix = requestURI.substring(idx + 1);
193             if (requestURISuffix.startsWith(JSESSIONID_URL_PREFIX))
194                 return extractBalancerMemberNameFromSessionId(requestURISuffix.substring(JSESSIONID_URL_PREFIX.length()));
195         }
196         return null;
197     }
198 
199     private String extractBalancerMemberNameFromSessionId(String sessionId)
200     {
201         int idx = sessionId.lastIndexOf(".");
202         if (idx > 0)
203         {
204             String sessionIdSuffix = sessionId.substring(idx + 1);
205             return sessionIdSuffix.length() > 0 ? sessionIdSuffix : null;
206         }
207         return null;
208     }
209 
210     @Override
211     protected String filterResponseHeader(HttpServletRequest request, String headerName, String headerValue)
212     {
213         if (_proxyPassReverse && REVERSE_PROXY_HEADERS.contains(headerName))
214         {
215             URI locationURI = URI.create(headerValue).normalize();
216             if (locationURI.isAbsolute() && isBackendLocation(locationURI))
217             {
218                 String newURI = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort();
219                 String component = locationURI.getRawPath();
220                 if (component != null)
221                     newURI += component;
222                 component = locationURI.getRawQuery();
223                 if (component != null)
224                     newURI += "?" + component;
225                 component = locationURI.getRawFragment();
226                 if (component != null)
227                     newURI += "#" + component;
228                 return URI.create(newURI).normalize().toString();
229             }
230         }
231         return headerValue;
232     }
233 
234     private boolean isBackendLocation(URI locationURI)
235     {
236         for (BalancerMember balancerMember : _balancerMembers)
237         {
238             URI backendURI = balancerMember.getBackendURI();
239             if (backendURI.getHost().equals(locationURI.getHost()) &&
240                     backendURI.getScheme().equals(locationURI.getScheme())
241                     && backendURI.getPort() == locationURI.getPort())
242             {
243                 return true;
244             }
245         }
246         return false;
247     }
248 
249     @Override
250     public boolean validateDestination(String host, int port)
251     {
252         return true;
253     }
254 
255     private static class BalancerMember
256     {
257         private final String _name;
258         private final String _proxyTo;
259         private final URI _backendURI;
260 
261         public BalancerMember(String name, String proxyTo)
262         {
263             _name = name;
264             _proxyTo = proxyTo;
265             _backendURI = URI.create(_proxyTo).normalize();
266         }
267 
268         public String getName()
269         {
270             return _name;
271         }
272 
273         public String getProxyTo()
274         {
275             return _proxyTo;
276         }
277 
278         public URI getBackendURI()
279         {
280             return _backendURI;
281         }
282 
283         @Override
284         public String toString()
285         {
286             return String.format("%s[name=%s,proxyTo=%s]", getClass().getSimpleName(), _name, _proxyTo);
287         }
288 
289         @Override
290         public int hashCode()
291         {
292             return _name.hashCode();
293         }
294 
295         @Override
296         public boolean equals(Object obj)
297         {
298             if (this == obj)
299                 return true;
300             if (obj == null)
301                 return false;
302             if (getClass() != obj.getClass())
303                 return false;
304             BalancerMember that = (BalancerMember)obj;
305             return _name.equals(that._name);
306         }
307     }
308 }