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.servlets;
20  
21  import java.net.MalformedURLException;
22  import java.net.URI;
23  import java.net.URISyntaxException;
24  import java.util.Collection;
25  import java.util.Collections;
26  import java.util.HashSet;
27  import java.util.Iterator;
28  import java.util.LinkedList;
29  import java.util.List;
30  import java.util.Set;
31  import java.util.concurrent.atomic.AtomicInteger;
32  
33  import javax.servlet.ServletConfig;
34  import javax.servlet.ServletException;
35  import javax.servlet.UnavailableException;
36  import javax.servlet.http.Cookie;
37  import javax.servlet.http.HttpServletRequest;
38  
39  import org.eclipse.jetty.http.HttpURI;
40  import org.eclipse.jetty.server.Request;
41  
42  /**
43   * 6
44   */
45  public class BalancerServlet extends ProxyServlet
46  {
47  
48      private static final class BalancerMember
49      {
50  
51          private String _name;
52  
53          private String _proxyTo;
54  
55          private HttpURI _backendURI;
56  
57          public BalancerMember(String name, String proxyTo)
58          {
59              super();
60              _name = name;
61              _proxyTo = proxyTo;
62              _backendURI = new HttpURI(_proxyTo);
63          }
64  
65          public String getProxyTo()
66          {
67              return _proxyTo;
68          }
69  
70          public HttpURI getBackendURI()
71          {
72              return _backendURI;
73          }
74  
75          @Override
76          public String toString()
77          {
78              return "BalancerMember [_name=" + _name + ", _proxyTo=" + _proxyTo + "]";
79          }
80  
81          @Override
82          public int hashCode()
83          {
84              final int prime = 31;
85              int result = 1;
86              result = prime * result + ((_name == null)?0:_name.hashCode());
87              return result;
88          }
89  
90          @Override
91          public boolean equals(Object obj)
92          {
93              if (this == obj)
94                  return true;
95              if (obj == null)
96                  return false;
97              if (getClass() != obj.getClass())
98                  return false;
99              BalancerMember other = (BalancerMember)obj;
100             if (_name == null)
101             {
102                 if (other._name != null)
103                     return false;
104             }
105             else if (!_name.equals(other._name))
106                 return false;
107             return true;
108         }
109 
110     }
111 
112     private static final class RoundRobinIterator implements Iterator<BalancerMember>
113     {
114 
115         private BalancerMember[] _balancerMembers;
116 
117         private AtomicInteger _index;
118 
119         public RoundRobinIterator(Collection<BalancerMember> balancerMembers)
120         {
121             _balancerMembers = (BalancerMember[])balancerMembers.toArray(new BalancerMember[balancerMembers.size()]);
122             _index = new AtomicInteger(-1);
123         }
124 
125         public boolean hasNext()
126         {
127             return true;
128         }
129 
130         public BalancerMember next()
131         {
132             BalancerMember balancerMember = null;
133             while (balancerMember == null)
134             {
135                 int currentIndex = _index.get();
136                 int nextIndex = (currentIndex + 1) % _balancerMembers.length;
137                 if (_index.compareAndSet(currentIndex,nextIndex))
138                 {
139                     balancerMember = _balancerMembers[nextIndex];
140                 }
141             }
142             return balancerMember;
143         }
144 
145         public void remove()
146         {
147             throw new UnsupportedOperationException();
148         }
149 
150     }
151 
152     private static final String BALANCER_MEMBER_PREFIX = "BalancerMember.";
153 
154     private static final List<String> FORBIDDEN_CONFIG_PARAMETERS;
155     static
156     {
157         List<String> params = new LinkedList<String>();
158         params.add("HostHeader");
159         params.add("whiteList");
160         params.add("blackList");
161         FORBIDDEN_CONFIG_PARAMETERS = Collections.unmodifiableList(params);
162     }
163 
164     private static final List<String> REVERSE_PROXY_HEADERS;
165     static
166     {
167         List<String> params = new LinkedList<String>();
168         params.add("Location");
169         params.add("Content-Location");
170         params.add("URI");
171         REVERSE_PROXY_HEADERS = Collections.unmodifiableList(params);
172     }
173 
174     private static final String JSESSIONID = "jsessionid";
175 
176     private static final String JSESSIONID_URL_PREFIX = JSESSIONID + "=";
177 
178     private boolean _stickySessions;
179 
180     private Set<BalancerMember> _balancerMembers = new HashSet<BalancerMember>();
181 
182     private boolean _proxyPassReverse;
183 
184     private RoundRobinIterator _roundRobinIterator;
185 
186     @Override
187     public void init(ServletConfig config) throws ServletException
188     {
189         validateConfig(config);
190         super.init(config);
191         initStickySessions(config);
192         initBalancers(config);
193         initProxyPassReverse(config);
194         postInit();
195     }
196 
197     private void validateConfig(ServletConfig config) throws ServletException
198     {
199         @SuppressWarnings("unchecked")
200         List<String> initParameterNames = Collections.list(config.getInitParameterNames());
201         for (String initParameterName : initParameterNames)
202         {
203             if (FORBIDDEN_CONFIG_PARAMETERS.contains(initParameterName))
204             {
205                 throw new UnavailableException(initParameterName + " not supported in " + getClass().getName());
206             }
207         }
208     }
209 
210     private void initStickySessions(ServletConfig config) throws ServletException
211     {
212         _stickySessions = "true".equalsIgnoreCase(config.getInitParameter("StickySessions"));
213     }
214 
215     private void initBalancers(ServletConfig config) throws ServletException
216     {
217         Set<String> balancerNames = getBalancerNames(config);
218         for (String balancerName : balancerNames)
219         {
220             String memberProxyToParam = BALANCER_MEMBER_PREFIX + balancerName + ".ProxyTo";
221             String proxyTo = config.getInitParameter(memberProxyToParam);
222             if (proxyTo == null || proxyTo.trim().length() == 0)
223             {
224                 throw new UnavailableException(memberProxyToParam + " parameter is empty.");
225             }
226             _balancerMembers.add(new BalancerMember(balancerName,proxyTo));
227         }
228     }
229 
230     private void initProxyPassReverse(ServletConfig config)
231     {
232         _proxyPassReverse = "true".equalsIgnoreCase(config.getInitParameter("ProxyPassReverse"));
233     }
234 
235     private void postInit()
236     {
237         _roundRobinIterator = new RoundRobinIterator(_balancerMembers);
238     }
239 
240     private Set<String> getBalancerNames(ServletConfig config) throws ServletException
241     {
242         Set<String> names = new HashSet<String>();
243         @SuppressWarnings("unchecked")
244         List<String> initParameterNames = Collections.list(config.getInitParameterNames());
245         for (String initParameterName : initParameterNames)
246         {
247             if (!initParameterName.startsWith(BALANCER_MEMBER_PREFIX))
248             {
249                 continue;
250             }
251             int endOfNameIndex = initParameterName.lastIndexOf(".");
252             if (endOfNameIndex <= BALANCER_MEMBER_PREFIX.length())
253             {
254                 throw new UnavailableException(initParameterName + " parameter does not provide a balancer member name");
255             }
256             names.add(initParameterName.substring(BALANCER_MEMBER_PREFIX.length(),endOfNameIndex));
257         }
258         return names;
259     }
260 
261     @Override
262     protected HttpURI proxyHttpURI(HttpServletRequest request, String uri) throws MalformedURLException
263     {
264         BalancerMember balancerMember = selectBalancerMember(request);
265         try
266         {
267             URI dstUri = new URI(balancerMember.getProxyTo() + "/" + uri).normalize();
268             return new HttpURI(dstUri.toString());
269         }
270         catch (URISyntaxException e)
271         {
272             throw new MalformedURLException(e.getMessage());
273         }
274     }
275 
276     private BalancerMember selectBalancerMember(HttpServletRequest request)
277     {
278         BalancerMember balancerMember = null;
279         if (_stickySessions)
280         {
281             String name = getBalancerMemberNameFromSessionId(request);
282             if (name != null)
283             {
284                 balancerMember = findBalancerMemberByName(name);
285                 if (balancerMember != null)
286                 {
287                     return balancerMember;
288                 }
289             }
290         }
291         return _roundRobinIterator.next();
292     }
293 
294     private BalancerMember findBalancerMemberByName(String name)
295     {
296         BalancerMember example = new BalancerMember(name,"");
297         for (BalancerMember balancerMember : _balancerMembers)
298         {
299             if (balancerMember.equals(example))
300             {
301                 return balancerMember;
302             }
303         }
304         return null;
305     }
306 
307     private String getBalancerMemberNameFromSessionId(HttpServletRequest request)
308     {
309         String name = getBalancerMemberNameFromSessionCookie(request);
310         if (name == null)
311         {
312             name = getBalancerMemberNameFromURL(request);
313         }
314         return name;
315     }
316 
317     private String getBalancerMemberNameFromSessionCookie(HttpServletRequest request)
318     {
319         Cookie[] cookies = request.getCookies();
320         String name = null;
321         for (Cookie cookie : cookies)
322         {
323             if (JSESSIONID.equalsIgnoreCase(cookie.getName()))
324             {
325                 name = extractBalancerMemberNameFromSessionId(cookie.getValue());
326                 break;
327             }
328         }
329         return name;
330     }
331 
332     private String getBalancerMemberNameFromURL(HttpServletRequest request)
333     {
334         String name = null;
335         String requestURI = request.getRequestURI();
336         int idx = requestURI.lastIndexOf(";");
337         if (idx != -1)
338         {
339             String requestURISuffix = requestURI.substring(idx);
340             if (requestURISuffix.startsWith(JSESSIONID_URL_PREFIX))
341             {
342                 name = extractBalancerMemberNameFromSessionId(requestURISuffix.substring(JSESSIONID_URL_PREFIX.length()));
343             }
344         }
345         return name;
346     }
347 
348     private String extractBalancerMemberNameFromSessionId(String sessionId)
349     {
350         String name = null;
351         int idx = sessionId.lastIndexOf(".");
352         if (idx != -1)
353         {
354             String sessionIdSuffix = sessionId.substring(idx + 1);
355             name = (sessionIdSuffix.length() > 0)?sessionIdSuffix:null;
356         }
357         return name;
358     }
359 
360     @Override
361     protected String filterResponseHeaderValue(String headerName, String headerValue, HttpServletRequest request)
362     {
363         if (_proxyPassReverse && REVERSE_PROXY_HEADERS.contains(headerName))
364         {
365             HttpURI locationURI = new HttpURI(headerValue);
366             if (isAbsoluteLocation(locationURI) && isBackendLocation(locationURI))
367             {
368                 Request jettyRequest = (Request)request;
369                 URI reverseUri;
370                 try
371                 {
372                     reverseUri = new URI(jettyRequest.getRootURL().append(locationURI.getCompletePath()).toString()).normalize();
373                     return reverseUri.toURL().toString();
374                 }
375                 catch (Exception e)
376                 {
377                     _log.warn("Not filtering header response",e);
378                     return headerValue;
379                 }
380             }
381         }
382         return headerValue;
383     }
384 
385     private boolean isBackendLocation(HttpURI locationURI)
386     {
387         for (BalancerMember balancerMember : _balancerMembers)
388         {
389             HttpURI backendURI = balancerMember.getBackendURI();
390             if (backendURI.getHost().equals(locationURI.getHost()) && backendURI.getScheme().equals(locationURI.getScheme())
391                     && backendURI.getPort() == locationURI.getPort())
392             {
393                 return true;
394             }
395         }
396         return false;
397     }
398 
399     private boolean isAbsoluteLocation(HttpURI locationURI)
400     {
401         return locationURI.getHost() != null;
402     }
403 
404     @Override
405     public String getHostHeader()
406     {
407         throw new UnsupportedOperationException("HostHeader not supported in " + getClass().getName());
408     }
409 
410     @Override
411     public void setHostHeader(String hostHeader)
412     {
413         throw new UnsupportedOperationException("HostHeader not supported in " + getClass().getName());
414     }
415 
416     @Override
417     public boolean validateDestination(String host, String path)
418     {
419         return true;
420     }
421 
422 }