/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
package org.apache.portals.applications.webcontent.proxy.impl;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CookieStore;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.methods.HttpTrace;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.routing.HttpRoutePlanner;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.impl.cookie.BasicClientCookie;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;
import org.apache.portals.applications.webcontent.proxy.HttpReverseProxyConstants;
import org.apache.portals.applications.webcontent.proxy.HttpReverseProxyException;
import org.apache.portals.applications.webcontent.proxy.HttpReverseProxyNotFoundException;
import org.apache.portals.applications.webcontent.proxy.HttpReverseProxyPathMapper;
import org.apache.portals.applications.webcontent.proxy.HttpReverseProxyPathMapperProvider;
import org.apache.portals.applications.webcontent.proxy.HttpReverseProxyService;
import org.apache.portals.applications.webcontent.proxy.ReverseProxyRewritingContext;
import org.apache.portals.applications.webcontent.proxy.ReverseProxyRewritingContextAware;
import org.apache.portals.applications.webcontent.proxy.SSOSiteCredentials;
import org.apache.portals.applications.webcontent.proxy.SSOSiteCredentialsProvider;
import org.apache.portals.applications.webcontent.rewriter.ParserAdaptor;
import org.apache.portals.applications.webcontent.rewriter.Rewriter;
import org.apache.portals.applications.webcontent.rewriter.RewriterController;
import org.apache.portals.applications.webcontent.rewriter.rules.Ruleset;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* HTTP Reverse Proxy Service Implementation
*
* @version $Id: RewritableHttpReverseProxyServiceImpl.java 902970 2010-01-25 20:53:32Z woonsan $
*/
public class RewritableHttpReverseProxyServiceImpl implements HttpReverseProxyService
{
private static Logger log = LoggerFactory.getLogger(RewritableHttpReverseProxyServiceImpl.class);
/**
* Proxy path mapper provider
*/
private HttpReverseProxyPathMapperProvider proxyPathMapperProvider;
/**
* Forced host header value
*/
private String hostHeaderValue;
/**
* forced local base url. e.g., "/webcontent/rproxy".
*/
private String localBaseURL;
/**
* SchemeRegistry
*/
private SchemeRegistry schemeRegistry;
/**
* The multithreaded connection manager for performance.
*/
private ClientConnectionManager connectionManager;
/**
* HTTP Connection Manager Parameters
*/
private HttpParams connectionManagerParams;
/**
* HTTP Client Parameters
*/
private HttpParams clientParams;
/**
* HTTP Route Planner
*/
private HttpRoutePlanner httpRoutePlanner;
public RewritableHttpReverseProxyServiceImpl(HttpReverseProxyPathMapperProvider proxyPathMapperProvider)
{
this.proxyPathMapperProvider = proxyPathMapperProvider;
}
public void setHostHeaderValue(String hostHeaderValue)
{
this.hostHeaderValue = hostHeaderValue;
}
public void setLocalBaseURL(String localBaseURL)
{
this.localBaseURL = localBaseURL;
}
public void setClientParams(HttpParams clientParams)
{
this.clientParams = clientParams;
}
public void setSchemeRegistry(SchemeRegistry schemeRegistry)
{
this.schemeRegistry = schemeRegistry;
}
public void setConnectionManagerParams(HttpParams connectionManagerParams)
{
this.connectionManagerParams = connectionManagerParams;
}
public void setHttpRoutePlanner(HttpRoutePlanner httpRoutePlanner)
{
this.httpRoutePlanner = httpRoutePlanner;
}
public void initialize()
{
if (clientParams == null)
{
clientParams = new BasicHttpParams();
}
if (connectionManagerParams == null)
{
connectionManagerParams = new BasicHttpParams();
}
if (schemeRegistry == null)
{
schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
schemeRegistry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));
}
connectionManager = new ThreadSafeClientConnManager(connectionManagerParams, schemeRegistry);
}
public void destroy()
{
if (connectionManager != null)
{
connectionManager.shutdown();
}
}
public void invoke(HttpServletRequest request, HttpServletResponse response) throws HttpReverseProxyException, IOException
{
// proxyPathMapper can be injected by using request attribute.
HttpReverseProxyPathMapper proxyPathMapper = (HttpReverseProxyPathMapper) request.getAttribute(HttpReverseProxyConstants.PATH_MAPPER);
String localPathInfo = request.getPathInfo();
if (localPathInfo.indexOf('/', 1) == -1)
{
localPathInfo = localPathInfo + "/";
}
if (proxyPathMapper == null)
{
proxyPathMapper = proxyPathMapperProvider.findMapper(localPathInfo);
}
if (proxyPathMapper == null)
{
throw new HttpReverseProxyNotFoundException("Proxy configuration is not defined for " + localPathInfo);
}
if (hostHeaderValue == null)
{
if (request.getServerPort() == 80)
{
hostHeaderValue = request.getServerName();
}
else
{
hostHeaderValue = request.getServerName() + ":" + request.getServerPort();
}
}
String proxyTargetURL = proxyPathMapper.getRemoteURL(localPathInfo);
if (proxyTargetURL == null)
{
throw new HttpReverseProxyNotFoundException("Cannot translate the location path info into remote URL. " + localPathInfo);
}
String queryString = request.getQueryString();
if (queryString != null)
{
proxyTargetURL = new StringBuilder(proxyTargetURL.length() + 1 + queryString.length()).append(proxyTargetURL).append('?').append(queryString).toString();
}
final List<org.apache.http.cookie.Cookie> responseSetCookies = new ArrayList<org.apache.http.cookie.Cookie>();
// create http client for each request...
DefaultHttpClient httpClient = new DefaultHttpClient(connectionManager, clientParams)
{
@Override
protected CookieStore createCookieStore()
{
return new BasicCookieStore()
{
@Override
public void addCookie(org.apache.http.cookie.Cookie cookie)
{
responseSetCookies.add(cookie);
}
};
}
};
if (httpRoutePlanner != null)
{
httpClient.setRoutePlanner(httpRoutePlanner);
}
// redirection should be adjusted with local host header...
httpClient.getParams().setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, false);
HttpRequestBase httpRequest = null;
String method = request.getMethod();
if (HttpGet.METHOD_NAME.equals(method))
{
httpRequest = new HttpGet(proxyTargetURL);
}
else if (HttpHead.METHOD_NAME.equals(method))
{
httpRequest = new HttpHead(proxyTargetURL);
}
else if (HttpPost.METHOD_NAME.equals(method))
{
httpRequest = new HttpPost(proxyTargetURL);
long contentLength = NumberUtils.toLong(request.getHeader(HTTP.CONTENT_LEN));
if (contentLength > 0L)
{
HttpEntity entity = new InputStreamEntity(request.getInputStream(), contentLength);
((HttpPost) httpRequest).setEntity(entity);
}
}
else if (HttpPut.METHOD_NAME.equals(method))
{
httpRequest = new HttpPut(proxyTargetURL);
long contentLength = NumberUtils.toLong(request.getHeader(HTTP.CONTENT_LEN));
if (contentLength > 0L)
{
HttpEntity entity = new InputStreamEntity(request.getInputStream(), contentLength);
((HttpPost) httpRequest).setEntity(entity);
}
}
else if (HttpDelete.METHOD_NAME.equals(method))
{
httpRequest = new HttpDelete(proxyTargetURL);
}
else if (HttpOptions.METHOD_NAME.equals(method))
{
httpRequest = new HttpOptions(proxyTargetURL);
}
else if (HttpTrace.METHOD_NAME.equals(method))
{
httpRequest = new HttpHead(proxyTargetURL);
}
else
{
throw new HttpReverseProxyException("Unsupported method: " + method);
}
// set sso credentials if available
List<SSOSiteCredentials> credsList = getSSOSiteCredentials(proxyTargetURL, httpClient, request);
if (credsList != null && !credsList.isEmpty())
{
SSOSiteCredentials firstCreds = credsList.get(0);
if (firstCreds.isFormAuthentication() && StringUtils.equals(firstCreds.getBaseURL(), proxyTargetURL))
{
httpRequest = new HttpPost(proxyTargetURL);
List <NameValuePair> formParams = new ArrayList<NameValuePair>();
formParams.add(new BasicNameValuePair(firstCreds.getFormUserField(), firstCreds.getUsername()));
formParams.add(new BasicNameValuePair(firstCreds.getFormPwdField(), firstCreds.getPassword()));
((HttpPost) httpRequest).setEntity(new UrlEncodedFormEntity(formParams));
}
else
{
for (SSOSiteCredentials creds : credsList)
{
AuthScope authScope = new AuthScope(creds.getHost(), creds.getPort(), creds.getRealm(), creds.getScheme());
Credentials usernamePwdCreds = new UsernamePasswordCredentials(creds.getUsername(), creds.getPassword());
httpClient.getCredentialsProvider().setCredentials(authScope, usernamePwdCreds);
}
}
}
// pass most headers to proxy target...
for (Enumeration enumHeaderNames = request.getHeaderNames(); enumHeaderNames.hasMoreElements(); )
{
String headerName = (String) enumHeaderNames.nextElement();
if (StringUtils.equalsIgnoreCase(headerName, HTTP.CONTENT_LEN))
continue;
if (StringUtils.equalsIgnoreCase(headerName, HTTP.TARGET_HOST))
continue;
for (Enumeration enumHeaderValues = request.getHeaders(headerName); enumHeaderValues.hasMoreElements(); )
{
String headerValue = (String) enumHeaderValues.nextElement();
httpRequest.addHeader(headerName, headerValue);
}
}
Map<String, String> defaultRequestHeaders = proxyPathMapper.getDefaultRequestHeaders();
if (defaultRequestHeaders != null)
{
for (Map.Entry<String, String> entry : defaultRequestHeaders.entrySet())
{
httpRequest.setHeader(entry.getKey(), entry.getValue());
}
}
CookieStore cookieStore = httpClient.getCookieStore();
if (cookieStore != null)
{
Map<String, String> defaultRequestCookies = proxyPathMapper.getDefaultRequestCookies();
if (defaultRequestCookies != null)
{
for (Map.Entry<String, String> entry : defaultRequestCookies.entrySet())
{
cookieStore.addCookie(new BasicClientCookie(entry.getKey(), entry.getValue()));
}
}
}
HttpEntity httpEntity = null;
try
{
HttpResponse httpResponse = httpClient.execute(httpRequest);
httpEntity = httpResponse.getEntity();
String rewriterContextPath = localBaseURL;
if (rewriterContextPath == null)
{
rewriterContextPath = request.getContextPath() + request.getServletPath();
}
int statusCode = httpResponse.getStatusLine().getStatusCode();
// Check if the proxy response is a redirect
if (statusCode >= HttpServletResponse.SC_MULTIPLE_CHOICES /* 300 */
&& statusCode < HttpServletResponse.SC_NOT_MODIFIED /* 304 */)
{
String location = null;
Header locationHeader = httpResponse.getFirstHeader(HttpReverseProxyConstants.HTTP_HEADER_LOCATION);
if (locationHeader != null)
{
location = locationHeader.getValue();
}
if (location == null)
{
throw new HttpReverseProxyException("Recieved status code is " + statusCode + " but no " + HttpReverseProxyConstants.HTTP_HEADER_LOCATION + " header was found in the response");
}
// Modify the redirect to go to this proxy servlet rather that the proxied host
// FYI, according to rfc2616, "Location" header value must be an absolute URI.
String localPath = proxyPathMapper.getLocalPath(location);
// if the current proxy path mapper cannot map the remote location to local path, then
// try to find out a possible path mapper instead one more...
if (localPath == null)
{
HttpReverseProxyPathMapper proxyPathMapperByLocation = proxyPathMapperProvider.findMapperByRemoteURL(location);
if (proxyPathMapperByLocation != null)
{
localPath = proxyPathMapperByLocation.getLocalPath(location);
}
}
String redirectLocation = null;
if (localPath == null)
{
if (log.isWarnEnabled())
{
log.warn("Cannot translate the redirect location to local path. {}", location);
}
redirectLocation = location;
}
else
{
redirectLocation = rewriterContextPath + localPath;
}
if (!responseSetCookies.isEmpty())
{
addResponseCookies(request, response, responseSetCookies, proxyPathMapper, rewriterContextPath);
}
response.sendRedirect(redirectLocation);
return;
}
else if (statusCode == HttpServletResponse.SC_NOT_MODIFIED)
{
// 304 needs special handling. See:
// http://www.ics.uci.edu/pub/ietf/http/rfc1945.html#Code304
// We get a 304 whenever passed an 'If-Modified-Since'
// header and the data on disk has not changed; server
// responds w/ a 304 saying I'm not going to send the
// body because the file has not changed.
response.setIntHeader(HTTP.CONTENT_LEN, 0);
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
if (!responseSetCookies.isEmpty())
{
addResponseCookies(request, response, responseSetCookies, proxyPathMapper, rewriterContextPath);
}
return;
}
else
{
// Pass the response code back to the client
response.setStatus(statusCode);
if (httpEntity != null)
{
boolean rewritable = false;
Rewriter rewriter = null;
ParserAdaptor parserAdaptor = null;
RewriterController rewriterController = proxyPathMapperProvider.getRewriterController(proxyPathMapper);
if (rewriterController != null)
{
parserAdaptor = createParserAdaptor(rewriterController, httpEntity);
if (parserAdaptor != null)
{
rewriter = createRewriter(rewriterController, proxyPathMapper);
rewritable = (rewriter != null);
}
}
// Pass response headers back to the client
Header [] headerArrayResponse = httpResponse.getAllHeaders();
for (Header header : headerArrayResponse)
{
String headerName = header.getName();
if (rewritable && StringUtils.equalsIgnoreCase(headerName, HTTP.CONTENT_LEN))
continue;
if (StringUtils.startsWithIgnoreCase(headerName, "Set-Cookie"))
continue;
String headerValue = header.getValue();
if (StringUtils.equalsIgnoreCase(headerName, HTTP.TARGET_HOST))
{
response.setHeader(headerName, hostHeaderValue);
}
else
{
response.setHeader(headerName, headerValue);
}
}
if (!responseSetCookies.isEmpty())
{
addResponseCookies(request, response, responseSetCookies, proxyPathMapper, rewriterContextPath);
}
// Send the content to the client
writeHttpEntityToClient(response, httpEntity, proxyPathMapper, rewriterContextPath, localPathInfo, rewriter, parserAdaptor);
}
}
}
catch (IOException e)
{
if (log.isDebugEnabled())
{
log.error("IOException occurred during execution for " + proxyTargetURL, e);
}
else
{
log.error("IOException occurred during execution for {} {}", proxyTargetURL, e);
}
httpRequest.abort();
httpEntity = null;
throw e;
}
catch (Exception e)
{
if (log.isDebugEnabled())
{
log.error("Exception occurred during execution for " + proxyTargetURL, e);
}
else
{
log.error("Exception occurred during execution for {} {}", proxyTargetURL, e);
}
httpRequest.abort();
httpEntity = null;
throw new HttpReverseProxyException(e);
}
finally
{
if (httpEntity != null)
{
httpEntity.consumeContent();
}
}
}
private void addResponseCookies(HttpServletRequest request, HttpServletResponse response, List<org.apache.http.cookie.Cookie> responseSetCookies, HttpReverseProxyPathMapper proxyPathMapper, String rewriterContextPath)
{
boolean isSecureRequest = request.isSecure();
Set<String> includes = proxyPathMapper.getRewriteCookiePathIncludes();
Set<String> excludes = proxyPathMapper.getRewriteCookiePathExcludes();
boolean includesEmpty = (includes == null || includes.isEmpty());
boolean excludesEmpty = (excludes == null || excludes.isEmpty());
boolean allEmpty = (includesEmpty && excludesEmpty);
String rewrittenCookiePath = rewriterContextPath + proxyPathMapper.getLocalBasePath();
for (org.apache.http.cookie.Cookie cookie : responseSetCookies)
{
String cookieName = cookie.getName();
Cookie responseCookie = new Cookie(cookieName, cookie.getValue());
responseCookie.setVersion(cookie.getVersion());
responseCookie.setComment(cookie.getComment());
Date expireDate = cookie.getExpiryDate();
if (expireDate != null)
{
int maxAgeSeconds = (int) ((expireDate.getTime() - System.currentTimeMillis()) / 1000L);
responseCookie.setMaxAge(maxAgeSeconds);
}
responseCookie.setSecure(isSecureRequest && cookie.isSecure());
responseCookie.setVersion(cookie.getVersion());
if ((allEmpty) || (!includesEmpty && includes.contains(cookieName)) || (!excludesEmpty && !excludes.contains(cookieName)))
{
responseCookie.setPath(rewrittenCookiePath);
}
response.addCookie(responseCookie);
}
}
private void writeHttpEntityToClient(HttpServletResponse response,
HttpEntity httpEntity,
HttpReverseProxyPathMapper proxyPathMapper,
String rewriterContextPath,
String localPathInfo,
Rewriter rewriter,
ParserAdaptor parserAdaptor) throws Exception
{
InputStream in = null;
Reader reader = null;
OutputStream out = null;
Writer writer = null;
try
{
in = httpEntity.getContent();
// According to javadoc of httpclient, getResponseBodyAsStream() can return null
// if the response has no body.
if (in != null)
{
out = response.getOutputStream();
if (rewriter == null || parserAdaptor == null)
{
IOUtils.copy(in, out);
out.flush();
}
else
{
boolean gzipEncoded = false;
Header contentEncodingHeader = httpEntity.getContentEncoding();
if (contentEncodingHeader != null)
{
gzipEncoded = StringUtils.equalsIgnoreCase("gzip", contentEncodingHeader.getValue());
}
if (parserAdaptor instanceof ReverseProxyRewritingContextAware)
{
ReverseProxyRewritingContext rewritingContext =
new DefaultReverseProxyRewritingContext(proxyPathMapper, proxyPathMapperProvider, rewriterContextPath);
((ReverseProxyRewritingContextAware) parserAdaptor).setReverseProxyRewritingContext(rewritingContext);
}
String responseCharSet = EntityUtils.getContentCharSet(httpEntity);
if (responseCharSet != null)
{
reader = new InputStreamReader(gzipEncoded ? new GZIPInputStream(in) : in, responseCharSet);
writer = new OutputStreamWriter(gzipEncoded ? new GZIPOutputStream(out) : out, responseCharSet);
}
else
{
reader = new InputStreamReader(gzipEncoded ? new GZIPInputStream(in) : in);
writer = new OutputStreamWriter(gzipEncoded ? new GZIPOutputStream(out) : out);
}
rewriter.setBaseUrl(rewriterContextPath + localPathInfo);
rewriter.rewrite(parserAdaptor, reader, writer);
writer.flush();
}
}
}
finally
{
if (reader != null)
{
IOUtils.closeQuietly(reader);
}
if (in != null)
{
IOUtils.closeQuietly(in);
}
if (writer != null)
{
IOUtils.closeQuietly(writer);
}
if (out != null)
{
IOUtils.closeQuietly(out);
}
}
}
private Rewriter createRewriter(RewriterController rewriterController, HttpReverseProxyPathMapper proxyPathMapper) throws Exception
{
Ruleset rewriterRuleset = proxyPathMapperProvider.getRewriterRuleset(proxyPathMapper);
if (rewriterRuleset == null)
{
return rewriterController.createRewriter();
}
else
{
return rewriterController.createRewriter(rewriterRuleset);
}
}
private ParserAdaptor createParserAdaptor(RewriterController rewriterController, HttpEntity httpEntity) throws Exception
{
String contentType = null;
Header contentTypeHeader = httpEntity.getContentType();
if (contentTypeHeader != null)
{
contentType = contentTypeHeader.getValue();
}
if (contentType == null)
{
return null;
}
String mimeType = contentType;
int offset = mimeType.indexOf(';');
if (offset > 0)
{
mimeType = mimeType.substring(0, offset).trim();
}
return rewriterController.createParserAdaptor(mimeType);
}
private List<SSOSiteCredentials> getSSOSiteCredentials(String siteURL, DefaultHttpClient httpClient, HttpServletRequest request)
{
SSOSiteCredentialsProvider credsProvider = (SSOSiteCredentialsProvider) request.getAttribute(HttpReverseProxyConstants.SSO_SITE_CREDENTIALS_PROVIDER);
if (credsProvider == null)
{
HttpSession session = request.getSession(false);
if (session != null)
{
credsProvider = (SSOSiteCredentialsProvider) session.getAttribute(HttpReverseProxyConstants.SSO_SITE_CREDENTIALS_PROVIDER);
}
}
if (credsProvider == null)
{
return null;
}
else
{
return credsProvider.getSSOCredentials(request, siteURL);
}
}
}