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         Cookie[] cookies = request.getCookies();
179         if (cookies != null)
180         {
181             for (Cookie cookie : cookies)
182             {
183                 if (JSESSIONID.equalsIgnoreCase(cookie.getName()))
184                     return extractBalancerMemberNameFromSessionId(cookie.getValue());
185             }
186         }
187         return null;
188     }
189 
190     private String getBalancerMemberNameFromURL(HttpServletRequest request)
191     {
192         String requestURI = request.getRequestURI();
193         int idx = requestURI.lastIndexOf(";");
194         if (idx > 0)
195         {
196             String requestURISuffix = requestURI.substring(idx + 1);
197             if (requestURISuffix.startsWith(JSESSIONID_URL_PREFIX))
198                 return extractBalancerMemberNameFromSessionId(requestURISuffix.substring(JSESSIONID_URL_PREFIX.length()));
199         }
200         return null;
201     }
202 
203     private String extractBalancerMemberNameFromSessionId(String sessionId)
204     {
205         int idx = sessionId.lastIndexOf(".");
206         if (idx > 0)
207         {
208             String sessionIdSuffix = sessionId.substring(idx + 1);
209             return sessionIdSuffix.length() > 0 ? sessionIdSuffix : null;
210         }
211         return null;
212     }
213 
214     @Override
215     protected String filterResponseHeader(HttpServletRequest request, String headerName, String headerValue)
216     {
217         if (_proxyPassReverse && REVERSE_PROXY_HEADERS.contains(headerName))
218         {
219             URI locationURI = URI.create(headerValue).normalize();
220             if (locationURI.isAbsolute() && isBackendLocation(locationURI))
221             {
222                 String newURI = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort();
223                 String component = locationURI.getRawPath();
224                 if (component != null)
225                     newURI += component;
226                 component = locationURI.getRawQuery();
227                 if (component != null)
228                     newURI += "?" + component;
229                 component = locationURI.getRawFragment();
230                 if (component != null)
231                     newURI += "#" + component;
232                 return URI.create(newURI).normalize().toString();
233             }
234         }
235         return headerValue;
236     }
237 
238     private boolean isBackendLocation(URI locationURI)
239     {
240         for (BalancerMember balancerMember : _balancerMembers)
241         {
242             URI backendURI = balancerMember.getBackendURI();
243             if (backendURI.getHost().equals(locationURI.getHost()) &&
244                     backendURI.getScheme().equals(locationURI.getScheme())
245                     && backendURI.getPort() == locationURI.getPort())
246             {
247                 return true;
248             }
249         }
250         return false;
251     }
252 
253     @Override
254     public boolean validateDestination(String host, int port)
255     {
256         return true;
257     }
258 
259     private static class BalancerMember
260     {
261         private final String _name;
262         private final String _proxyTo;
263         private final URI _backendURI;
264 
265         public BalancerMember(String name, String proxyTo)
266         {
267             _name = name;
268             _proxyTo = proxyTo;
269             _backendURI = URI.create(_proxyTo).normalize();
270         }
271 
272         public String getName()
273         {
274             return _name;
275         }
276 
277         public String getProxyTo()
278         {
279             return _proxyTo;
280         }
281 
282         public URI getBackendURI()
283         {
284             return _backendURI;
285         }
286 
287         @Override
288         public String toString()
289         {
290             return String.format("%s[name=%s,proxyTo=%s]", getClass().getSimpleName(), _name, _proxyTo);
291         }
292 
293         @Override
294         public int hashCode()
295         {
296             return _name.hashCode();
297         }
298 
299         @Override
300         public boolean equals(Object obj)
301         {
302             if (this == obj)
303                 return true;
304             if (obj == null)
305                 return false;
306             if (getClass() != obj.getClass())
307                 return false;
308             BalancerMember that = (BalancerMember)obj;
309             return _name.equals(that._name);
310         }
311     }
312 }