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