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
19package org.eclipse.jetty.servlets;
20
21import java.net.MalformedURLException;
22import java.net.URI;
23import java.net.URISyntaxException;
24import java.util.Collection;
25import java.util.Collections;
26import java.util.HashSet;
27import java.util.Iterator;
28import java.util.LinkedList;
29import java.util.List;
30import java.util.Set;
31import java.util.concurrent.atomic.AtomicInteger;
32
33import javax.servlet.ServletConfig;
34import javax.servlet.ServletException;
35import javax.servlet.UnavailableException;
36import javax.servlet.http.Cookie;
37import javax.servlet.http.HttpServletRequest;
38
39import org.eclipse.jetty.http.HttpURI;
40import org.eclipse.jetty.server.Request;
41
42/**
43 * 6
44 */
45public 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}
423