// // ======================================================================== // Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php // // You may elect to redistribute this code under either of these licenses. // ======================================================================== // package org.eclipse.jetty.proxy; import java.net.URI; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; import javax.servlet.ServletException; import javax.servlet.UnavailableException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.util.URIUtil; public class BalancerServlet extends ProxyServlet { private static final String BALANCER_MEMBER_PREFIX = "balancerMember."; private static final List<String> FORBIDDEN_CONFIG_PARAMETERS; static { List<String> params = new LinkedList<>(); params.add("hostHeader"); params.add("whiteList"); params.add("blackList"); FORBIDDEN_CONFIG_PARAMETERS = Collections.unmodifiableList(params); } private static final List<String> REVERSE_PROXY_HEADERS; static { List<String> params = new LinkedList<>(); params.add("Location"); params.add("Content-Location"); params.add("URI"); REVERSE_PROXY_HEADERS = Collections.unmodifiableList(params); } private static final String JSESSIONID = "jsessionid"; private static final String JSESSIONID_URL_PREFIX = JSESSIONID + "="; private final List<BalancerMember> _balancerMembers = new ArrayList<>(); private final AtomicLong counter = new AtomicLong(); private boolean _stickySessions; private boolean _proxyPassReverse; @Override public void init() throws ServletException { validateConfig(); super.init(); initStickySessions(); initBalancers(); initProxyPassReverse(); } private void validateConfig() throws ServletException { for (String initParameterName : Collections.list(getServletConfig().getInitParameterNames())) { if (FORBIDDEN_CONFIG_PARAMETERS.contains(initParameterName)) { throw new UnavailableException(initParameterName + " not supported in " + getClass().getName()); } } } private void initStickySessions() { _stickySessions = Boolean.parseBoolean(getServletConfig().getInitParameter("stickySessions")); } private void initBalancers() throws ServletException { Set<BalancerMember> members = new HashSet<>(); for (String balancerName : getBalancerNames()) { String memberProxyToParam = BALANCER_MEMBER_PREFIX + balancerName + ".proxyTo"; String proxyTo = getServletConfig().getInitParameter(memberProxyToParam); if (proxyTo == null || proxyTo.trim().length() == 0) throw new UnavailableException(memberProxyToParam + " parameter is empty."); members.add(new BalancerMember(balancerName, proxyTo)); } _balancerMembers.addAll(members); } private void initProxyPassReverse() { _proxyPassReverse = Boolean.parseBoolean(getServletConfig().getInitParameter("proxyPassReverse")); } private Set<String> getBalancerNames() throws ServletException { Set<String> names = new HashSet<>(); for (String initParameterName : Collections.list(getServletConfig().getInitParameterNames())) { if (!initParameterName.startsWith(BALANCER_MEMBER_PREFIX)) continue; int endOfNameIndex = initParameterName.lastIndexOf("."); if (endOfNameIndex <= BALANCER_MEMBER_PREFIX.length()) throw new UnavailableException(initParameterName + " parameter does not provide a balancer member name"); names.add(initParameterName.substring(BALANCER_MEMBER_PREFIX.length(), endOfNameIndex)); } return names; } @Override protected String rewriteTarget(HttpServletRequest request) { BalancerMember balancerMember = selectBalancerMember(request); if (_log.isDebugEnabled()) _log.debug("Selected {}", balancerMember); String path = request.getRequestURI(); String query = request.getQueryString(); if (query != null) path += "?" + query; return URI.create(balancerMember.getProxyTo() + "/" + path).normalize().toString(); } private BalancerMember selectBalancerMember(HttpServletRequest request) { if (_stickySessions) { String name = getBalancerMemberNameFromSessionId(request); if (name != null) { BalancerMember balancerMember = findBalancerMemberByName(name); if (balancerMember != null) return balancerMember; } } int index = (int)(counter.getAndIncrement() % _balancerMembers.size()); return _balancerMembers.get(index); } private BalancerMember findBalancerMemberByName(String name) { for (BalancerMember balancerMember : _balancerMembers) { if (balancerMember.getName().equals(name)) return balancerMember; } return null; } private String getBalancerMemberNameFromSessionId(HttpServletRequest request) { String name = getBalancerMemberNameFromSessionCookie(request); if (name == null) name = getBalancerMemberNameFromURL(request); return name; } private String getBalancerMemberNameFromSessionCookie(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (JSESSIONID.equalsIgnoreCase(cookie.getName())) return extractBalancerMemberNameFromSessionId(cookie.getValue()); } } return null; } private String getBalancerMemberNameFromURL(HttpServletRequest request) { String requestURI = request.getRequestURI(); int idx = requestURI.lastIndexOf(";"); if (idx > 0) { String requestURISuffix = requestURI.substring(idx + 1); if (requestURISuffix.startsWith(JSESSIONID_URL_PREFIX)) return extractBalancerMemberNameFromSessionId(requestURISuffix.substring(JSESSIONID_URL_PREFIX.length())); } return null; } private String extractBalancerMemberNameFromSessionId(String sessionId) { int idx = sessionId.lastIndexOf("."); if (idx > 0) { String sessionIdSuffix = sessionId.substring(idx + 1); return sessionIdSuffix.length() > 0 ? sessionIdSuffix : null; } return null; } @Override protected String filterServerResponseHeader(HttpServletRequest request, Response serverResponse, String headerName, String headerValue) { if (_proxyPassReverse && REVERSE_PROXY_HEADERS.contains(headerName)) { URI locationURI = URI.create(headerValue).normalize(); if (locationURI.isAbsolute() && isBackendLocation(locationURI)) { StringBuilder newURI = URIUtil.newURIBuilder(request.getScheme(), request.getServerName(), request.getServerPort()); String component = locationURI.getRawPath(); if (component != null) newURI.append(component); component = locationURI.getRawQuery(); if (component != null) newURI.append('?').append(component); component = locationURI.getRawFragment(); if (component != null) newURI.append('#').append(component); return URI.create(newURI.toString()).normalize().toString(); } } return headerValue; } private boolean isBackendLocation(URI locationURI) { for (BalancerMember balancerMember : _balancerMembers) { URI backendURI = balancerMember.getBackendURI(); if (backendURI.getHost().equals(locationURI.getHost()) && backendURI.getScheme().equals(locationURI.getScheme()) && backendURI.getPort() == locationURI.getPort()) { return true; } } return false; } @Override public boolean validateDestination(String host, int port) { return true; } private static class BalancerMember { private final String _name; private final String _proxyTo; private final URI _backendURI; public BalancerMember(String name, String proxyTo) { _name = name; _proxyTo = proxyTo; _backendURI = URI.create(_proxyTo).normalize(); } public String getName() { return _name; } public String getProxyTo() { return _proxyTo; } public URI getBackendURI() { return _backendURI; } @Override public String toString() { return String.format("%s[name=%s,proxyTo=%s]", getClass().getSimpleName(), _name, _proxyTo); } @Override public int hashCode() { return _name.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; BalancerMember that = (BalancerMember)obj; return _name.equals(that._name); } } }