/* 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/>.
*
* ResourceServlet.java
*
* Created on 19 January 2006, 13:51
*/
package no.sesat.commons.resourcefeed;
import org.apache.log4j.Logger;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Set;
import java.util.Map;
import java.util.HashSet;
import java.util.Arrays;
import java.util.Calendar;
/** Resource Provider.
* Serves configuration files (properties, xml), css, gifs, jpgs, javascript,
* classes, jar files and velocity templates for search-portal.
* Css, images, and javascript require direct access from client.
*
*
* @version $Id$
*/
public final class ResourceServlet extends HttpServlet {
private static final Logger LOG = Logger.getLogger(ResourceServlet.class);
private static final String REMOTE_ADDRESS_KEY = "REMOTE_ADDR";
private static final String ERR_RESTRICTED_AREA = "<strong>Restricted Area!</strong>";
private static final String ERR_TRIED_TO_ACCESS = " tried to access Resource servlet!";
private static final String ERR_NOT_FOUND = "Failed to find resource ";
private static final String ERR_TRIED_TO_CROSS_REFERENCE = " tried to cross-reference resource!";
private static final String DEBUG_DEFAULT_MODIFCATION_TIMESTAMP = "Default modified timestamp set to ";
private static final String DEBUG_CLIENT_IP = "Client ipaddress ";
private static final Map<String,String> CONTENT_TYPES = new HashMap<String,String>();
private static final Map<String,String> CONTENT_PATHS = new HashMap<String,String>();
private static final Set<String> RESTRICTED = new HashSet<String>();
private static final Calendar CROWNING_OF_HAILE_SELASSIE = Calendar.getInstance();
private long defaultLastModified = 0;
private String[] ipaddressesAllowed = new String[]{};
private Set<String> paths;
private ServletConfig servletConfig;
static {
// The different extension to content type mappings
// XXX is there an opensource library to do this?
CONTENT_TYPES.put("properties", "text/plain");
CONTENT_TYPES.put("xml", "text/xml");
CONTENT_TYPES.put("css", "text/css");
CONTENT_TYPES.put("js", "text/javascript");
CONTENT_TYPES.put("jpg", "image/jpeg");
CONTENT_TYPES.put("gif", "image/gif");
CONTENT_TYPES.put("png", "image/png");
CONTENT_TYPES.put("ico", "image/x-icon");
CONTENT_TYPES.put("vm", "text/plain");
CONTENT_TYPES.put("html", "text/plain");
CONTENT_TYPES.put("class", "application/java");
CONTENT_TYPES.put("jar", "application/java-archive");
// Things that don't expire
CROWNING_OF_HAILE_SELASSIE.set(Calendar.YEAR, 9999);
// call it in a safe environment (internals are not thread safe).
CROWNING_OF_HAILE_SELASSIE.getTimeInMillis();
}
/**
* {@inheritDoc}
*/
@Override
public String getServletInfo() {
return "Servlet responsible for serving resources. Goes in hand with search-portal/site-spi";
}
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings("unchecked")
public void init(final ServletConfig config) {
this.servletConfig = config;
defaultLastModified = System.currentTimeMillis();
LOG.warn(DEBUG_DEFAULT_MODIFCATION_TIMESTAMP + defaultLastModified);
final String allowed = config.getInitParameter("ipaddresses.allowed");
LOG.warn("allowing ipaddresses " + allowed);
if (null != allowed && allowed.length() >0) {
ipaddressesAllowed = allowed.split(",");
}
final String restricted = config.getInitParameter("resources.restricted");
LOG.warn("restricted resources " + restricted);
if (null != restricted && restricted.length()>0) {
RESTRICTED.addAll(Arrays.asList(restricted.split(",")));
}
final String paths = config.getInitParameter("content.paths");
LOG.warn("content path mappings " + paths);
if (null != paths && paths.length()>0) {
final String[] pathArr = paths.split(",");
for (String path : pathArr) {
final String[] pair = path.split("=");
CONTENT_PATHS.put(pair[0], pair[1]);
}
}
this.paths = servletConfig.getServletContext().getResourcePaths("/WEB-INF/lib");
LOG.warn("ResourcePaths are");
for(String s : this.paths){
LOG.warn(' ' + s);
}
}
/**
* Processes requests for both HTTP <code>GET</code> and <code>POST</code> methods.
* This servlet ignores URL parameters and POST content, as all the information is in the path,
* so it really doesn't matter if it is a GET or POST.
*
* Checks:
* - resource exists,
* - correct path is being used,
* - configuration/template resources are only accessed by schibsted machines,
*
* The resource is served to the ServletOutputStream byte by byte from
* getClass().getResourceAsStream(..)
*
* @param request servlet request
* @param response servlet response
* @throws javax.servlet.ServletException if ServletException occurs
* @throws java.io.IOException if IOException occurs
*/
protected void processRequest(
final HttpServletRequest request,
final HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8"); // correct encoding
// Get resource name. Also strip the version number out of the resource
final String pathInfo;
final String directory;
if(null != request.getPathInfo()){
// simple scenerio where servlet-mapping was a prefix match.
pathInfo = request.getPathInfo();
directory = request.getServletPath();
}else{
// servlet-mapping was extension based
pathInfo = request.getServletPath().substring(request.getServletPath().indexOf('/', 1));
directory = request.getServletPath().substring(0, request.getServletPath().indexOf('/', 1));
}
LOG.debug("pathInfo: " + pathInfo + " ; directory: " + directory);
assert null != pathInfo : "Invalid resource " + pathInfo;
final String configName = pathInfo.replaceAll("/(\\d)+/","/");
assert 0 < configName.trim().length() : "Invalid resource " + pathInfo;
assert 0 < configName.lastIndexOf('.') : "Invalid resource extension " + pathInfo;
if (configName != null && configName.trim().length() > 0) {
final String extension = configName.substring(configName.lastIndexOf('.') + 1).toLowerCase();
assert null != extension : "Invalid resource extension" + pathInfo;
assert 0 < extension.trim().length() : "Invalid resource extension " + pathInfo;
final String ipAddr = null != request.getAttribute(REMOTE_ADDRESS_KEY)
? (String) request.getAttribute(REMOTE_ADDRESS_KEY)
: request.getRemoteAddr();
// Content-Type
response.setContentType(CONTENT_TYPES.get(extension) + ";charset=UTF-8");
// Path check. Resource can only be loaded through correct path.
if (null != CONTENT_PATHS.get(extension) && directory.indexOf(CONTENT_PATHS.get(extension)) >= 0) {
// ok, check configuration resources are private.
LOG.trace(DEBUG_CLIENT_IP + ipAddr);
final boolean restricted = RESTRICTED.contains(extension);
if (restricted && !isIpAllowed(ipAddr)) {
response.setContentType("text/html;charset=UTF-8");
response.getOutputStream().print(ERR_RESTRICTED_AREA);
LOG.warn(ipAddr + ERR_TRIED_TO_ACCESS);
} else {
serveResource(configName, restricted, request, response);
}
} else {
// not allowed to cross-reference resources.
response.sendError(HttpServletResponse.SC_NOT_FOUND);
LOG.warn(ipAddr + ERR_TRIED_TO_CROSS_REFERENCE);
}
}
}
/**
* {@inheritDoc}
*/
@Override
protected void doGet(
final HttpServletRequest request,
final HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
/**
* {@inheritDoc}
*/
@Override
protected void doPost(
final HttpServletRequest request,
final HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
/** Assigned to the time when the servlet is initialised via the init(ServletConfig) method.
* Any redeployment of the skin results in an update in the last-modified response header.
* Editing the files "in-place" on disk will not have any effect on the last-modified header.
*
* @param req incoming HttpServletRequest request
* @return last-modified header (in milliseconds)
**/
@Override
protected long getLastModified(final HttpServletRequest req) {
return defaultLastModified;
}
private void serveResource(
final String configName,
final boolean restricted,
final HttpServletRequest request,
final HttpServletResponse response)
throws ServletException, IOException {
InputStream is = null;
LOG.debug("serveResource(" + configName + ", request, response)");
try {
is = configName.endsWith(".jar")
? getJarStream(configName)
: ResourceServlet.class.getResourceAsStream(configName);
if (is != null) {
// Write response headers before response data according to javadoc for HttpServlet.html#doGet(..)
// Allow any public URL to be cached indefinitely.
// Each jvm restart alters the number that appears in the URL being enough to ensure
// nothing is cached across deployment versions.
if(!restricted){
response.setHeader("Cache-Control", "Public");
// We used to use Long.MAX_VALUE but http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1
// shows the year field can only be four digits long.
// see https://helpdesk.basefarm.com/bin/customer?action=listTicket&ticketId=274212
response.setDateHeader("Expires", CROWNING_OF_HAILE_SELASSIE.getTimeInMillis());
}else{
// never cache private resources
response.setHeader("Cache-Control", "no-cache");
}
// Avoid writing out the response body if it's a HEAD request or a GET that the browser has cache for
boolean writeBody = !"HEAD".equals(request.getMethod());
writeBody &= request.getDateHeader("If-Modified-Since") <= defaultLastModified;
if (writeBody) {
// Output the resource byte for byte
final OutputStream os = response.getOutputStream();
for (int b = is.read(); b >= 0; b = is.read()) {
os.write(b);
}
// commit response now
os.flush();
}
} else {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
LOG.info(ERR_NOT_FOUND + request.getPathInfo());
}
} finally {
if (is != null) {
is.close();
}
}
}
private InputStream getJarStream(final String resource) throws IOException {
final String baseName = resource.replace(".jar", "").replace("/", "");
LOG.debug("getJarStream(" + resource + ") [baseName:" + baseName + ']');
for (String path : paths) {
// Remove path, site name and version suffix.
final String jarName = path
.substring(path.lastIndexOf('/') + 1)
.replaceAll("-(\\d+\\.?)+(-SNAPSHOT).*\\.jar", "")
.replaceAll("^([\\p{Alnum}]+\\.?)+-", "");
if (LOG.isDebugEnabled()) {
LOG.debug("Checking against " + jarName);
}
if (jarName.equals(baseName)) {
LOG.warn("Loading jarfile " + path);
return servletConfig.getServletContext().getResource(path).openConnection().getInputStream();
}
}
return null;
}
/**
* Returns wether we allow the ipaddress or not.
* @param ipAddr the ipaddress to check.
*
* @return returns true if the ip address is trusted.
*/
private boolean isIpAllowed(final String ipAddr) {
boolean allowed =
ipAddr.startsWith("127.") || ipAddr.startsWith("10.") || ipAddr.startsWith("0:0:0:0:0:0:0:1%0");
for(String s : ipaddressesAllowed){
allowed |= ipAddr.startsWith(s);
}
return allowed;
}
}