/* Copyright (2006-2012) Schibsted ASA
* This file is part of Possom.
*
* Possom is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Possom is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Possom. If not, see <http://www.gnu.org/licenses/>.
*
* SiteLocatorFilter.java
*
* Created on 9 February 2006, 11:30
*/
package no.sesat.search.http.filters;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Locale;
import java.util.UUID;
import java.text.MessageFormat;
import java.util.Deque;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import javax.servlet.http.HttpSession;
import no.sesat.search.site.config.SiteConfiguration;
import no.sesat.search.site.config.UrlResourceLoader;
import no.sesat.search.site.Site;
import no.sesat.search.view.FindResource;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.time.StopWatch;
import org.apache.log4j.Logger;
import org.apache.log4j.MDC;
/** Loads the appropriate Site object in as a request attribute.
* Will redirect to correct (search-front-config) url for resources (css,images, javascript). <br/>
* Also responsible for logging each request and response like an apache access logfile.
*
*
* @version $Id$
*/
public final class SiteLocatorFilter implements Filter {
// Constants -----------------------------------------------------
private static final Logger LOG = Logger.getLogger(SiteLocatorFilter.class);
private static final Logger ACCESS_LOG = Logger.getLogger("no.sesat.Access");
private static final String ERR_NOT_FOUND = "Failed to find resource ";
private static final String ERR_UNCAUGHT_RUNTIME_EXCEPTION
= "Following runtime exception was let loose in tomcat against ";
private static final String INFO_USING_DEFAULT_LOCALE = " is falling back to the default locale ";
private static final String DEBUG_REQUESTED_VHOST = "Virtual host is ";
private static final String DEBUG_REDIRECTING_TO = " redirect to ";
private static final String WARN_FAULTY_BROWSER = "Site in datamodel does not match requested site. User agent is ";
private static final String PUBLISH_DIR = "/img/";
private static final String UNKNOWN = "unknown";
private static final String USER_REQUEST_QUEUE = "userRequestQueue";
private static final String USER_REQUEST_LOCK = "userRequestSemaphore";
private static final long WAIT_TIME = 5000;
private static final int REQUEST_QUEUE_SIZE = 5;
// Any request coming into Possom with /conf/ is immediately returned as a 404.
// It should have been directed to a skin.
private static final String CONFIGURATION_RESOURCE= "/conf/";
/** Changes to this list must also change the ProxyPass|ProxyPassReverse configuration in httpd.conf **/
private static final Collection<String> EXTERNAL_DIRS =
Collections.unmodifiableCollection(Arrays.asList(new String[]{
PUBLISH_DIR, "/css/", "/images/", "/javascript/"
}));
/** The context that we'll need to use every invocation of doFilter(..).
* @throws IllegalArgumentException when there exists no skin matching the siteContext.getSite() argument.
**/
public static final Site.Context SITE_CONTEXT = UrlResourceLoader.SITE_CONTEXT;
// Attributes ----------------------------------------------------
// The filter configuration object we are associated with. If
// this value is null, this filter instance is not currently
// configured.
private FilterConfig filterConfig = null;
private static final String LOCALE_DETAILS = "Locale details: Language: {0}, Country: {1} and Variant: {2}";
// Static --------------------------------------------------------
static String getRequestId(final ServletRequest servletRequest){
if(null == servletRequest.getAttribute("UNIQUE_ID")){
servletRequest.setAttribute("UNIQUE_ID", UUID.randomUUID().toString());
}
return (String)servletRequest.getAttribute("UNIQUE_ID");
}
/** The method to obtain the correct Site from the request.
* It only returns a site with a locale supported by that site.
** @param servletRequest
* @return the site instance. or null if no such skin has been deployed.
*/
public static Site getSite(final ServletRequest servletRequest) {
// find the current site. Since we are behind a ajp13 connection request.getServerName() won't work!
// httpd.conf needs:
// 1) "JkEnvVar SERVER_NAME" inside the virtual host directive.
// 2) "UseCanonicalName Off" to assign ServerName from client's request.
final String vhost = getServerName(servletRequest);
// Tweak the port if SERVER_PORT has been explicitly set. (We may have gone through Apache or Cisco LB).
final String correctedVhost = Site.SERVER_PORT > 0 && vhost.indexOf(':') > 0
? vhost.substring(0, vhost.indexOf(':') + 1) + Site.SERVER_PORT
: vhost;
LOG.trace(DEBUG_REQUESTED_VHOST + correctedVhost);
// Construct the site object off the browser's locale, even if it won't finally be used.
final Locale locale = servletRequest.getLocale();
final Site result;
try{
result = Site.valueOf(SITE_CONTEXT, correctedVhost, locale);
final SiteConfiguration.Context siteConfCxt = UrlResourceLoader.newSiteConfigurationContext(result);
final SiteConfiguration siteConf = SiteConfiguration.instanceOf(siteConfCxt);
servletRequest.setAttribute(SiteConfiguration.NAME_KEY, siteConf);
if(LOG.isTraceEnabled()){ // MessageFormat.format(..) is expensive
LOG.trace(MessageFormat.format(
LOCALE_DETAILS, locale.getLanguage(), locale.getCountry(), locale.getVariant()));
}
// Check if the browser's locale is supported by this skin. Use it if so.
if( siteConf.isSiteLocaleSupported(locale) ){
return result;
}
// Use the skin's default locale. For some reason that fails use JVM's default.
final String[] prefLocale = null != siteConf.getProperty(SiteConfiguration.SITE_LOCALE_DEFAULT)
? siteConf.getProperty(SiteConfiguration.SITE_LOCALE_DEFAULT).split("_")
: new String[]{Locale.getDefault().toString()};
switch(prefLocale.length){
case 3:
LOG.trace(result+INFO_USING_DEFAULT_LOCALE + prefLocale[0] + '_' + prefLocale[1] + '_' + prefLocale[2]);
return Site.valueOf(SITE_CONTEXT, correctedVhost, new Locale(prefLocale[0], prefLocale[1], prefLocale[2]));
case 2:
LOG.trace(result+INFO_USING_DEFAULT_LOCALE + prefLocale[0] + '_' + prefLocale[1]);
return Site.valueOf(SITE_CONTEXT, correctedVhost, new Locale(prefLocale[0], prefLocale[1]));
case 1:
default:
LOG.trace(result+INFO_USING_DEFAULT_LOCALE + prefLocale[0]);
return Site.valueOf(SITE_CONTEXT, correctedVhost, new Locale(prefLocale[0]));
}
}catch(IllegalArgumentException iae){
return null;
}
}
// Constructors --------------------------------------------------
/** Default constructor. **/
public SiteLocatorFilter() {
}
// Public --------------------------------------------------------
/** Will redirect to correct (search-config) url for resources (css,images, javascript).
*
* @param request The servlet request we are processing
* @param r The servlet response
* @param chain The filter chain we are processing
*
* @exception IOException if an input/output error occurs
* @exception ServletException if a servlet error occurs
*/
@Override
public void doFilter(
final ServletRequest request,
final ServletResponse r,
final FilterChain chain)
throws IOException, ServletException {
LOG.trace("doFilter(..)");
final StopWatch stopWatch = new StopWatch();
stopWatch.start();
final ServletResponse response = r instanceof HttpServletResponse
? new AccessLogResponse((HttpServletResponse)r)
: r;
try{
if(request instanceof HttpServletRequest) {
final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
final HttpServletResponse httpServletResponse = (HttpServletResponse) response;
if (httpServletRequest.getRequestURI().contains(CONFIGURATION_RESOURCE)){
/* We are looping, looking for a site search which does not exsist */
LOG.info("We are looping, looking for a site search which does not exist");
httpServletResponse.reset();
httpServletResponse.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
}
doBeforeProcessing(request, response);
logAccessRequest(request);
if (request instanceof HttpServletRequest) {
final HttpServletRequest req = (HttpServletRequest)request;
final HttpServletResponse res = (HttpServletResponse) response;
final Site site = (Site) req.getAttribute(Site.NAME_KEY);
final String uri = req.getRequestURI();
final String resource = uri;
final String rscDir = resource != null && resource.indexOf('/',1) >= 0
? resource.substring(0, resource.indexOf('/',1)+1)
: null;
if(isAccessAllowed(req)){
if (rscDir != null && EXTERNAL_DIRS.contains(rscDir)) {
// This URL does not belong to search-portal
final String url = FindResource.find(site, resource);
if (url != null) {
// Cache the client-resource redirects on a short (session-equivilant) period
res.setHeader("Cache-Control", "Public");
res.setDateHeader("Expires", System.currentTimeMillis() + 1000*60*10); // ten minutes
// send the redirect to where the resource really resides
res.sendRedirect(url);
LOG.trace(resource + DEBUG_REDIRECTING_TO + url);
}else if (resource.startsWith(PUBLISH_DIR)){
// XXX Why do we avoid sending 404 for publish resources?
res.sendError(HttpServletResponse.SC_NOT_FOUND);
if(resource.endsWith(".css")){
LOG.info(ERR_NOT_FOUND + resource);
}else{
LOG.error(ERR_NOT_FOUND + resource);
}
}
} else {
doChainFilter(chain, request, response);
}
}else{
// Forbidden client
res.sendError(HttpServletResponse.SC_FORBIDDEN);
}
} else {
doChainFilter(chain, request, response);
}
doAfterProcessing(request, response);
} catch (Exception e) {
// Don't let anything through without logging it.
// Otherwise it ends in a different logfile.
LOG.error(ERR_UNCAUGHT_RUNTIME_EXCEPTION);
for (Throwable t = e; t != null; t = t.getCause()) {
LOG.error(t.getMessage(), t);
}
throw new ServletException(e);
}finally{
logAccessResponse(request, response, stopWatch);
}
}
/**
* Return the filter configuration object for this filter.
* @return
*/
public FilterConfig getFilterConfig() {
return (filterConfig);
}
/**
* Set the filter configuration object for this filter.
*
* @param filterConfig The filter configuration object
*/
public void setFilterConfig(final FilterConfig filterConfig) {
this.filterConfig = filterConfig;
}
/**
* Destroy method for this filter
*
*/
@Override
public void destroy() {
}
/**
* Init method for this filter
*
*/
@Override
public void init(final FilterConfig filterConfig) {
this.filterConfig = filterConfig;
if (filterConfig != null) {
LOG.debug("Initializing filter");
}
}
/**
* Return a String representation of this object.
*/
@Override
public String toString() {
return filterConfig == null
? "ResourceRedirectFilter()"
: "ResourceRedirectFilter(" + filterConfig + ")";
}
// Package protected ---------------------------------------------
// Protected -----------------------------------------------------
// Private -------------------------------------------------------
private static void doChainFilter(
final FilterChain chain,
final ServletRequest request,
final ServletResponse response) throws IOException, ServletException {
if (request instanceof HttpServletRequest) {
doChainFilter(chain, (HttpServletRequest)request, (HttpServletResponse)response);
} else {
chain.doFilter(request, response);
}
}
private static void doChainFilter(
final FilterChain chain,
final HttpServletRequest request,
final HttpServletResponse response) throws IOException, ServletException {
final HttpSession session = request.getSession();
// fetch the user's deque
final Deque<ServletRequest> deque = getUsersDeque(session);
// lock to execute
final ReentrantLock lock = (ReentrantLock) session.getAttribute(USER_REQUEST_LOCK);
// deque has a time limit. start counting.
long timeLeft = WAIT_TIME;
try{
// attempt to join deque
if (deque.offerFirst(request)) {
timeLeft = tryLock(request, deque, lock, timeLeft);
}
if(lock.isHeldByCurrentThread()){
// waiting is over. and we can execute
chain.doFilter(request, response);
}else{
// we failed to execute. return 409 response.
if (response instanceof HttpServletResponse) {
LOG.warn(" -- response 409 " +
(0 < timeLeft
? "(More then " + REQUEST_QUEUE_SIZE + " requests already in queue)"
: "(Timeout: Waited " + WAIT_TIME + " ms)"));
response.sendError(HttpServletResponse.SC_CONFLICT);
}
}
}finally{
// take out of deque first
deque.remove(request);
// release the lock, waiting up the next request
if(lock.isHeldByCurrentThread()){ lock.unlock(); }
}
}
private static Deque<ServletRequest> getUsersDeque(final HttpSession session){
@SuppressWarnings("unchecked")
Deque<ServletRequest> deque = (BlockingDeque<ServletRequest>) session.getAttribute(USER_REQUEST_QUEUE);
// construct deque if necessary
if (null == deque) {
// it may be possible for duplicates across threads to be constructed here
deque = new LinkedBlockingDeque<ServletRequest>(REQUEST_QUEUE_SIZE);
session.setAttribute(USER_REQUEST_QUEUE, deque);
session.setAttribute(USER_REQUEST_LOCK, new ReentrantLock());
}
return deque;
}
private static long tryLock(
final HttpServletRequest request,
final Deque<ServletRequest> deque,
final Lock lock,
long timeLeft){
final long start = System.currentTimeMillis();
try {
do{
timeLeft = WAIT_TIME - (System.currentTimeMillis() - start);
// let's sleep. sleeping too long results in 409 response
if(0 >= timeLeft || !lock.tryLock(timeLeft, TimeUnit.MILLISECONDS)){
// we timed out or got the lock. waiting is over
break;
}else if(deque.peek() != request){
// we've acquired the lock but we're not at front of deque
// release the lock and try again
lock.unlock();
}
}while(deque.peek() != request);
}catch(InterruptedException ie){
LOG.error("Failed using user's lock", ie);
}
return timeLeft;
}
private void doBeforeProcessing(final ServletRequest request, final ServletResponse response)
throws IOException, ServletException {
LOG.trace("doBeforeProcessing()");
final Site site = getSite(request);
if(null != site){
request.setAttribute(Site.NAME_KEY, site);
request.setAttribute("startTime", FindResource.START_TIME);
MDC.put(Site.NAME_KEY, site.getName());
MDC.put("UNIQUE_ID", getRequestId(request));
/* Setting default encoding */
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
}else{
throw new ServletException("SiteLocatorFilter with no Site :-(");
}
}
private void doAfterProcessing(final ServletRequest request, final ServletResponse response)
throws IOException, ServletException {
LOG.trace("doAfterProcessing()");
//
// Write code here to process the request and/or response after
// the rest of the filter chain is invoked.
//
}
private static void logAccessRequest(final ServletRequest request){
final StringBuilder url = new StringBuilder();
final String referer;
final String method;
final String ip = request.getRemoteAddr();
final String userAgent;
final String sesamId;
final String sesamUser;
if(request instanceof HttpServletRequest){
final HttpServletRequest req = (HttpServletRequest)request;
url.append(req.getRequestURI() + (null != req.getQueryString() ? '?' + req.getQueryString() : ""));
referer = req.getHeader("Referer");
method = req.getMethod();
userAgent = req.getHeader("User-Agent");
sesamId = getCookieValue(req, "SesamID");
sesamUser = getCookieValue(req, "SesamUser");
}else{
for(@SuppressWarnings("unchecked")
Enumeration<String> en = request.getParameterNames(); en.hasMoreElements(); ){
final String param = en.nextElement();
url.append(param + '=' + request.getParameter(param));
if(en.hasMoreElements()){
url.append('&');
}
}
referer = method = userAgent = sesamId = sesamUser = UNKNOWN;
}
ACCESS_LOG.info("<request>"
+ "<url method=\"" + method + "\">" + StringEscapeUtils.escapeXml(url.toString()) + "</url>"
+ (null != referer ? "<referer>" + StringEscapeUtils.escapeXml(referer) + "</referer>" : "")
+ "<browser ipaddress=\"" + ip + "\">" + StringEscapeUtils.escapeXml(userAgent) + "</browser>"
+ "<user id=\"" + sesamId + "\">" + sesamUser + "</user>"
+ "</request>");
}
private static void logAccessResponse(
final ServletRequest request,
final ServletResponse response,
final StopWatch stopWatch){
final String code;
if(request instanceof HttpServletRequest){
final HttpServletRequest req = (HttpServletRequest)request;
}else{
}
if(response instanceof AccessLogResponse){
final AccessLogResponse res = (AccessLogResponse)response;
code = String.valueOf(res.getStatus());
}else{
code = UNKNOWN;
}
stopWatch.stop();
ACCESS_LOG.info("<response code=\"" + code + "\" time=\"" + stopWatch + "\"/>");
}
// probably apache commons could simplify this // duplicated in SearchServlet
private static String getCookieValue(final HttpServletRequest request, final String cookieName){
String value = "";
// Look in attributes (it could have already been updated this request)
if( null != request ){
// Look through cookies
if( null != request.getCookies() ){
for( Cookie c : request.getCookies()){
if( c.getName().equals( cookieName ) ){
value = c.getValue();
break;
}
}
}
}
return value;
}
private static String getServerName(final ServletRequest servletRequest){
// find the current site. Since we are behind a ajp13 connection request.getServerName() won't work!
// httpd.conf needs:
// 1) "JkEnvVar SERVER_NAME" inside the virtual host directive.
// 2) "UseCanonicalName Off" to assign ServerName from client's request.
return null != servletRequest.getAttribute("SERVER_NAME")
? (String) servletRequest.getAttribute("SERVER_NAME")
// falls back to this when not behind Apache. (Development machine).
: servletRequest.getServerName() + ":" + servletRequest.getServerPort();
}
private static boolean isAccessAllowed(final HttpServletRequest request){
final SiteConfiguration siteConf = (SiteConfiguration) request.getAttribute(SiteConfiguration.NAME_KEY);
final String allowedList = siteConf.getProperty(SiteConfiguration.ALLOW_LIST);
final String disallowedList = siteConf.getProperty(SiteConfiguration.DISALLOW_LIST);
final String ipaddress = request.getRemoteAddr();
boolean allowed = false;
boolean disallowed = false;
if(null != allowedList && 0 < allowedList.length()){
for(String allow : allowedList.split(",")){
allowed |= ipaddress.startsWith(allow);
}
}else{
allowed = true;
}
if(null != disallowedList && 0 < disallowedList.length()){
for(String disallow : disallowedList.split(",")){
disallowed |= ipaddress.startsWith(disallow);
}
}
return allowed && !disallowed;
}
private static class AccessLogResponse extends HttpServletResponseWrapper{
private int status = HttpServletResponse.SC_OK;
public AccessLogResponse(final HttpServletResponse response){
super(response);
}
@Override
public void setStatus(final int status){
super.setStatus(status);
this.status = status;
}
@Override
public void setStatus(final int status, final String msg){
super.setStatus(status, msg);
this.status = status;
}
@Override
public void sendError(final int sc) throws IOException{
super.sendError(sc);
status = sc;
}
@Override
public void sendError(final int sc, final String msg) throws IOException{
super.sendError(sc, msg);
status = sc;
}
@Override
public void sendRedirect(final String arg0) throws IOException {
super.sendRedirect(arg0);
this.status = HttpServletResponse.SC_FOUND;
}
public int getStatus(){
return status;
}
}
}