Commit 96ec7c0c authored by Fabien Viale's avatar Fabien Viale
Browse files

Request rewriter for proxyfied applications

 - add the rewriter as a parent handler to rewrite urls before they are handled by context handlers
 - wrote PCAProxyRule which rewrites the url if the Referer contains a proxyfied endpoint
 - use a cache mechanism in case the Referer does not contain a proxified endpoint but this referer has already been the target of a rewrite (case of css including other css)
  - add properties to disable the rewriter and control cache size
 - redirect GET request instead of rewriting them (prevents usage of referer cache for most requests)
parent f673aa0b
......@@ -73,6 +73,10 @@ public enum WebProperties implements PACommonProperties {
WEB_REDIRECT_HTTP_TO_HTTPS("web.redirect_http_to_https", PropertyType.BOOLEAN, "false"),
WEB_PCA_PROXY_REWRITE_ENABLED("web.pca.proxy.rewrite.enabled", PropertyType.BOOLEAN, "true"),
WEB_PCA_PROXY_REWRITE_REFERER_CACHE_SIZE("web.pca.proxy.rewrite.referer.cache.size", PropertyType.INTEGER, "10000"),
METADATA_CONTENT_TYPE("content.type", PropertyType.STRING),
......@@ -39,6 +39,12 @@ web.https.keystore.password=activeeon
# or not when HTTPS is used to communicate with the REST API
# Enable PCA Proxy rewriter to route requests coming from proxyfied applications
# PCA Proxy cache size to store referer information
# Uncomment and set the following settings if resource downloading must pass through a proxy
......@@ -61,6 +61,7 @@ dependencies {
runtime 'org.eclipse.jetty.websocket:websocket-server:9.2.29.v20191105'
runtime 'org.eclipse.jetty:jetty-webapp:9.2.29.v20191105'
runtime 'org.eclipse.jetty:jetty-rewrite:9.2.29.v20191105'
runtime "org.objectweb.proactive:programming-extension-pamr:${programmingVersion}"
......@@ -17,6 +17,7 @@ dependencies {
compile 'org.fusesource.jdbm:jdbm:2.0.1'
compile 'org.eclipse.jetty:jetty-webapp:9.2.29.v20191105'
compile 'org.eclipse.jetty:jetty-rewrite:9.2.29.v20191105'
compile "org.objectweb.proactive:programming-core:${programmingVersion}"
compile project(':common:common-api')
......@@ -31,10 +31,13 @@ import;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.DispatcherType;
import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.Logger;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
......@@ -116,7 +119,15 @@ public class JettyStarter {
HandlerList handlerList = new HandlerList();
HandlerList topLevelHandlerList = new HandlerList();
if (httpsEnabled && redirectHttpToHttps) {
ContextHandler redirectHandler = new ContextHandler();
redirectHandler.setHandler(new SecuredRedirectHandler());
if (WebProperties.JETTY_LOG_FILE.isSet()) {
String pathToJettyLogFile = FileStorageSupportFactory.relativeToHomeIfNotAbsolute(WebProperties.JETTY_LOG_FILE.getValueAsString());
......@@ -134,20 +145,34 @@ public class JettyStarter {
RequestLogHandler requestLogHandler = new RequestLogHandler();
if (httpsEnabled && redirectHttpToHttps) {
ContextHandler redirectHandler = new ContextHandler();
redirectHandler.setHandler(new SecuredRedirectHandler());
RewriteHandler rewriteHandler = null;
HandlerList contextHandlerList = null;
if (WebProperties.WEB_PCA_PROXY_REWRITE_ENABLED.getValueAsBoolean()) {
rewriteHandler = new RewriteHandler();
PCAProxyRule proxyRule = new PCAProxyRule();
rewriteHandler.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
contextHandlerList = new HandlerList();
} else {
contextHandlerList = topLevelHandlerList;
addWarsToHandlerList(contextHandlerList, defaultVirtualHost);
if (WebProperties.WEB_PCA_PROXY_REWRITE_ENABLED.getValueAsBoolean()) {
addWarsToHandlerList(handlerList, defaultVirtualHost);
String schedulerHost = ProActiveInet.getInstance().getHostname();
return startServer(server, schedulerHost, restPort, httpProtocol);
......@@ -283,7 +308,18 @@ public class JettyStarter {
private List<String> printDeployedApplications(Server server, String schedulerHost, int restPort,
String httpProtocol) {
HandlerList handlerList = (HandlerList) server.getHandler();
HandlerList handlerList = null;
if (WebProperties.WEB_PCA_PROXY_REWRITE_ENABLED.getValueAsBoolean()) {
HandlerList topLevelHandlerList = (HandlerList) server.getHandler();
for (Handler handler : topLevelHandlerList.getHandlers()) {
if (handler instanceof RewriteHandler) {
handlerList = (HandlerList) ((RewriteHandler) handler).getHandler();
} else {
handlerList = (HandlerList) server.getHandler();
ArrayList<String> applicationsUrls = new ArrayList<>();
if (handlerList.getHandlers() != null) {
for (Handler handler : handlerList.getHandlers()) {
* ProActive Parallel Suite(TM):
* The Open Source library for parallel and distributed
* Workflows & Scheduling, Orchestration, Cloud Automation
* and Big Data Analysis on Enterprise Grids & Clouds.
* Copyright (c) 2007 - 2017 ActiveEon
* Contact:
* This library is free software: you can redistribute it and/or
* modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation: version 3 of
* the License.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <>.
* If needed, contact us to obtain a release under GPL Version 2 or 3
* or a different license than the AGPL.
package org.ow2.proactive.utils;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.http.HttpHeaders;
import org.apache.log4j.Logger;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.rewrite.handler.Rule;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.URIUtil;
import org.ow2.proactive.web.WebProperties;
* PCAProxyRule allows rewriting requests coming from PCA proxyfied applications.
* It relies on the Referer header to determine where a request comes from
* It does not do any operation for requests which contains a Referer header not matching any proxified application url
* For example:
* A request /static/css/some.css is received with a Referer containing /cloud-automation-service/services/12/endpoints/my-endpoint/
* This request will be rewritten to
* /cloud-automation-service/services/12/endpoints/my-endpoint/static/css/some.css
* if the request is a GET request, the jetty server will issue a redirection (code 302 Found), and request handling will terminate.
* This allows the client browser to have a correct referer for further requests.
* If the request is not a GET request, the request handling will continue, using the rewritten uri, thus delegating to cloud-automation-service the request.
* In the rare case where the referer is incorrect (for example a css stylesheet witch includes another css with absolute path AND redirection strategy did not work),
* PCAProxyRule maintains a referer cache to keep track of requests which have already been rewritten, and will use this cache to determine the correct referer.
* @author ActiveEon Team
* @since 23/07/2020
public class PCAProxyRule extends Rule implements Rule.ApplyURI {
public static final String originalPathAttribute = "__path";
private final static String PCA_PROXY_REGEXP = "/cloud-automation-service/services/[0-9]+/endpoints/[^/]+/";
private final static Pattern pattern = Pattern.compile(PCA_PROXY_REGEXP);
private static final Logger logger = Logger.getLogger(PCAProxyRule.class);
private final int maximumCacheSize;
private final LinkedHashMap<String, String> referrerCache;
public PCAProxyRule() {
_terminating = false;
_handling = false;
maximumCacheSize = WebProperties.WEB_PCA_PROXY_REWRITE_REFERER_CACHE_SIZE.getValueAsInt();
referrerCache = new LinkedHashMap<String, String>(100, 0.75f, true) {
public boolean removeEldestEntry(Map.Entry eldest) {
return size() > maximumCacheSize;
public String matchAndApply(String target, HttpServletRequest request, HttpServletResponse response)
throws IOException {
if (logger.isTraceEnabled()) {
logger.trace("Analysing target: " + target);
String referer = request.getHeader(HttpHeaders.REFERER);
if (logger.isTraceEnabled()) {
logger.trace("Referer: " + referer);
if (referer != null && !referer.isEmpty()) {
URL refererUrl = null;
try {
refererUrl = new URL(referer);
} catch (Exception e) {
logger.warn("Invalid URL in referer : " + referer, e);
return target;
String refererPath = refererUrl.getPath();
Matcher matcherReferer = pattern.matcher(refererPath);
if (matcherReferer.find()) {
String endpointPath = refererPath.substring(matcherReferer.start(), matcherReferer.end());
return rewriteTarget(target, endpointPath, request, response);
} else if (referrerCache.containsKey(refererPath)) {
// the referer is a direct url which has already been handled and stored in our cache
// this is typically the case when a css file includes another css file
// in order to solve this problem, we reuse the previous endpoint stored in the cache and associated with this referer
String endpointPath = referrerCache.get(refererPath);
return rewriteTarget(target, endpointPath, request, response);
return target;
private String rewriteTarget(String target, String endpointPath, HttpServletRequest request,
HttpServletResponse response) {
Matcher matcherRequest = pattern.matcher(target);
if (logger.isDebugEnabled()) {
logger.debug("Endpoint found in referer: " + endpointPath);
if (!matcherRequest.find()) {
// request target does not point to the pca endpoint, we need to add it and remove the trailing slash
String newTarget = endpointPath.substring(0, endpointPath.length() - 1) + target;
if (logger.isDebugEnabled()) {
logger.debug(String.format("Rewrote %s to %s", target, newTarget));
referrerCache.put(target, endpointPath);
if (request.getMethod().equals(HttpMethod.GET.asString())) {
redirectGetRequest(target, endpointPath, request, response, newTarget);
return newTarget;
} else {
logger.trace("Target already contains endpoint");
return target;
private void redirectGetRequest(String target, String endpointPath, HttpServletRequest request,
HttpServletResponse response, String newTarget) {
// GET requests should be redirected while preserving all original parameters
// this allows the application to provide a correct Referer in most cases (thus not relying on the cache mechanism)
// Other type of requests redirection behaves erratically
try {
String newUri = endpointPath.substring(0, endpointPath.length() - 1) +
((Request) request).getUri().getCompletePath();
if (logger.isDebugEnabled()) {
logger.debug(String.format("Relocation URI %s", newUri));
(request instanceof Request ? (Request) request
: HttpChannel.getCurrentHttpChannel().getRequest()).setHandled(true);
} catch (IOException e) {
logger.error(String.format("Error while redirecting %s to %s", target, newTarget), e);
public void applyURI(Request request, String oldURI, String newURI) throws IOException {
String originalPath = URIUtil.encodePath((String) request.getAttribute(originalPathAttribute));
if (logger.isTraceEnabled()) {
logger.trace("originalPath: " + originalPath);
logger.trace("oldURI: " + oldURI);
logger.trace("newURI: " + newURI);
boolean isRedirection = !originalPath.equals(newURI);
if (isRedirection && oldURI.startsWith(originalPath)) {
String remaining = oldURI.substring(originalPath.length());
String uriRewritten = newURI + remaining;
if (logger.isDebugEnabled()) {
logger.debug(String.format("Rewrote URI %s to %s", oldURI, uriRewritten));
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment