View Javadoc

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