/*
* #%L
* Wisdom-Framework
* %%
* Copyright (C) 2013 - 2014 Wisdom Framework
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package org.wisdom.framework.filters;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import org.wisdom.api.configuration.Configuration;
import org.wisdom.api.http.HeaderNames;
import org.wisdom.api.http.Request;
import org.wisdom.api.http.Result;
import org.wisdom.api.interception.Filter;
import org.wisdom.api.interception.RequestContext;
import org.wisdom.api.router.Route;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
/**
* A filter acting as a load balancer between {@link org.wisdom.framework.filters
* .BalancerMember}. This implementation is made to fetch member from the service registry (and so are
* dynamic), but you can change this behavior. This balancer supports sticky session and reverse routing.
* However sticky session is limited by dynamism, and may not be enforced if the targeted member has
* left. If no members are bound to the balancer, the request is just delegated to the next filter.
* <p>
* To create an instance of {@link org.wisdom.framework.filters.BalancerFilter}, you need to override this class and
* declare it as a {@link org.wisdom.api.annotations.Service}. You can override most of its behavior. You have to
* manage the binding and unbinding of {@link org.wisdom.framework.filters.BalancerMember}.
*/
public class BalancerFilter extends ProxyFilter implements Filter {
/**
* The set of headers for reverse proxy management.
*/
private static final Set<String> REVERSE_PROXY_HEADERS = ImmutableSet.of(
HeaderNames.LOCATION,
HeaderNames.CONTENT_LOCATION
// URI ?
);
/**
* List of members.
*/
private final List<BalancerMember> members = new ArrayList<>();
/**
* The name of the balancer.
*/
private final String name;
/**
* Whether or not the balancer should support sticky session.
*/
private final boolean stickySession;
/**
* Whether or not the balancer handle reverse proxy.
*/
private final boolean proxyPassReverse;
/**
* Counter to return unique id.
*/
private final AtomicLong counter = new AtomicLong();
/**
* Creates a {@link org.wisdom.framework.filters.BalancerFilter} instance. This instance requires that the {@link
* BalancerFilter#getName()} method is implemented by the sub-class.
*/
public BalancerFilter() {
this.name = getName();
this.stickySession = getStickySession();
this.proxyPassReverse = getProxyPassReverse();
}
/**
* Creates a {@link org.wisdom.framework.filters.BalancerFilter} instance. Configuration is taken from the given
* configuration object.
*
* @param configuration the configuration object
*/
public BalancerFilter(Configuration configuration) {
super(configuration);
this.name = getName();
this.prefix = getPrefix();
this.stickySession = getStickySession();
this.proxyPassReverse = getProxyPassReverse();
}
/**
* A default implementation of the {@link ProxyFilter#getProxyTo()} method returning an empty String.
*
* @return an empty String
*/
@Override
protected final String getProxyTo() {
// Just there to not be null, and fail in the 'super' constructor.
return "";
}
/**
* Methods called on incoming request. If there are no members attached to this balancer, the request is
* processed using {@link org.wisdom.api.interception.RequestContext#proceed()}. Otherwise, a member is selected
* and the request is delegated.
*
* @param route the route
* @param context the filter context
* @return the result
* @throws Exception when the request cannot be handled correctly
*/
@Override
public Result call(Route route, RequestContext context) throws Exception {
if (getMembers().isEmpty()) {
return context.proceed();
} else {
return super.call(route, context);
}
}
private synchronized List<BalancerMember> getMembers() {
return new ArrayList<>(members);
}
/**
* Compute the destination URI. It picks a member (enforcing the sticky session if enabled), and computes the URI.
*
* @param rc the request content
* @return the new URI
* @throws URISyntaxException if the URI cannot be computed
*/
@Override
public URI rewriteURI(RequestContext rc) throws URISyntaxException {
Request request = rc.request();
BalancerMember member = selectBalancerMember(rc);
logger.debug("Selected {}", member.getName());
String path = request.path();
if (!path.startsWith(prefix)) {
return null;
}
return computeDestinationURI(
request,
path,
member.proxyTo(),
prefix
);
}
protected BalancerMember selectBalancerMember(RequestContext request) {
BalancerMember member;
if (stickySession) {
String balancer = request.context().session().get("_balancer");
if (balancer == null) {
// URL lookup (query string).
balancer = request.request().parameter("_balancer");
}
// A balancer hint was given.
if (balancer != null) {
member = getBalancerMember(balancer);
if (member != null) {
// Member still around.
return member;
}
}
// The member left, we can't ensure the sticky session.
logger.warn("Cannot enforce sticky session policy for {} - the member ({}) has left", request.request().uri(), balancer);
}
synchronized (this) {
int index = (int) (counter.getAndIncrement() % members.size());
member = members.get(index);
if (stickySession) {
request.context().session().put("_balancer", member.getName());
}
return member;
}
}
/**
* Callback that can be overridden to customize the header ot the request. This method implements the reverse
* routing. It updates URLs contained in the headers.
*
* @param context the request context
* @param headers the current set of headers, that need to be modified
*/
@Override
public void updateHeaders(RequestContext context, Multimap<String, String> headers) {
if (!proxyPassReverse) {
return;
}
for (Map.Entry<String, String> h : new LinkedHashSet<>(headers.entries())) {
if (REVERSE_PROXY_HEADERS.contains(h.getKey())) {
URI location = URI.create(h.getValue()).normalize();
if (location.isAbsolute() && isBackendLocation(location)) {
String initial = context.request().uri();
URI uri = URI.create(initial);
try {
URI newURI = new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(),
location.getPath(), location.getQuery(), location.getFragment());
headers.remove(h.getKey(), h.getValue());
headers.put(h.getKey(), newURI.toString());
} catch (URISyntaxException e) {
logger.error("Cannot manipulate the header {} (value={}) to enforce reverse routing", h
.getKey(), h.getValue(), e);
}
}
}
}
}
private boolean isBackendLocation(URI location) {
for (BalancerMember member : getMembers()) {
URI backendURI = URI.create(member.proxyTo()).normalize();
if (backendURI.getHost().equals(location.getHost())
&& backendURI.getScheme().equals(location.getScheme())
&& backendURI.getPort() == location.getPort()) {
return true;
}
}
return false;
}
private BalancerMember getBalancerMember(String balancer) {
for (BalancerMember member : getMembers()) {
if (member.getName().equals(balancer)) {
return member;
}
}
return null;
}
/**
* Gets the balancer name.
*
* @return the name
*/
public String getName() {
if (configuration == null) {
throw new IllegalArgumentException("The balancer name must be set (either " +
"by overriding, or configuration)");
} else {
return configuration.getOrDie("name");
}
}
/**
* Checks whether or not the sticky session support is enabled (false by default).
*
* @return {@code true} when sticky sessions are enabled, {@code false} otherwise.
*/
public boolean getStickySession() {
if (configuration == null) {
return false;
} else {
return configuration.getBooleanWithDefault("stickySession", false);
}
}
/**
* Checks whether or not the reverse routing support is enabled (false by default).
*
* @return {@code true} when reverse routing is enabled, {@code false} otherwise.
*/
public boolean getProxyPassReverse() {
if (configuration == null) {
return false;
} else {
return configuration.getBooleanWithDefault("proxyPassReverse", false);
}
}
/**
* Adds a new member.
*
* @param member the member.
*/
public synchronized void addMember(BalancerMember member) {
if (member.getBalancerName().equals(name)) {
logger.info("Adding balancer member '{}' to balancer '{}'", member.getName(), name);
members.add(member);
}
}
/**
* Removes a member.
*
* @param member the member.
*/
public synchronized void removeMember(BalancerMember member) {
if (members.remove(member)) {
logger.info("Removing balancer member '{}' from balancer '{}'", member.getName(), name);
}
}
}