/* The contents of this file are subject to the license and copyright terms
* detailed in the license directory at the root of the source tree (also
* available online at http://fedora-commons.org/license/).
*/
package fedora.server.access.dissemination;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.sql.Timestamp;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.Logger;
import fedora.common.Constants;
import fedora.server.Context;
import fedora.server.ReadOnlyContext;
import fedora.server.Server;
import fedora.server.errors.InitializationException;
import fedora.server.errors.authorization.AuthzException;
import fedora.server.errors.authorization.AuthzOperationalException;
import fedora.server.errors.servletExceptionExtensions.RootException;
import fedora.server.security.BackendPolicies;
import fedora.server.storage.ContentManagerParams;
import fedora.server.storage.DOManager;
import fedora.server.storage.DOReader;
import fedora.server.storage.ExternalContentManager;
import fedora.server.storage.types.Datastream;
import fedora.server.storage.types.DatastreamMediation;
import fedora.server.storage.types.MIMETypedStream;
import fedora.server.storage.types.Property;
import fedora.server.utilities.ServerUtility;
/**
* This servlet acts as a proxy to resolve the physical location of datastreams.
*
* <p>It requires a single parameter named <code>id</code> that denotes the
* temporary id of the requested datastresm. This id is in the form of a
* DateTime stamp. The servlet will perform an in-memory hashtable lookup
* using the temporary id to obtain the actual physical location of the
* datastream and then return the contents of the datastream as a MIME-typed
* stream. This servlet is invoked primarily by external mechanisms needing to
* retrieve the contents of a datastream.
*
* <p>The servlet also requires that an external mechanism request a datastream
* within a finite time interval of the tempID's creation. This is to lessen the
* risk of unauthorized access. The time interval within which a mechanism must
* respond is set by the Fedora configuration parameter named
* datastreamMediationLimit and is specified in milliseconds. If this
* parameter is not supplied it defaults to 5000 milliseconds.
*
* @author Ross Wayland
*/
public class DatastreamResolverServlet
extends HttpServlet {
/** Logger for this class. */
private static final Logger LOG =
Logger.getLogger(DatastreamResolverServlet.class.getName());
private static final long serialVersionUID = 1L;
private static Server s_server;
private static DOManager m_manager;
private static Hashtable dsRegistry;
private static int datastreamMediationLimit;
private static final String HTML_CONTENT_TYPE = "text/html";
private static String fedoraServerHost;
private static String fedoraServerPort;
private static String fedoraServerRedirectPort;
/**
* Initialize servlet.
*
* @throws ServletException
* If the servlet cannot be initialized.
*/
public void init() throws ServletException {
try {
s_server =
Server.getInstance(new File(Constants.FEDORA_HOME), false);
fedoraServerPort = s_server.getParameter("fedoraServerPort");
fedoraServerRedirectPort =
s_server.getParameter("fedoraRedirectPort");
fedoraServerHost = s_server.getParameter("fedoraServerHost");
m_manager =
(DOManager) s_server
.getModule("fedora.server.storage.DOManager");
String expireLimit =
s_server.getParameter("datastreamMediationLimit");
if (expireLimit == null || expireLimit.equalsIgnoreCase("")) {
LOG.info("datastreamMediationLimit unspecified, using default "
+ "of 5 seconds");
datastreamMediationLimit = 5000;
} else {
datastreamMediationLimit = new Integer(expireLimit).intValue();
LOG.info("datastreamMediationLimit: "
+ datastreamMediationLimit);
}
} catch (InitializationException ie) {
throw new ServletException("Unable to get an instance of Fedora server "
+ "-- " + ie.getMessage());
} catch (Throwable th) {
LOG.error("Error initializing servlet", th);
}
}
private static final boolean contains(String[] array, String item) {
boolean contains = false;
for (String element : array) {
if (element.equals(item)) {
contains = true;
break;
}
}
return contains;
}
public static final String ACTION_LABEL = "Resolve Datastream";
/**
* Processes the servlet request and resolves the physical location of the
* specified datastream.
*
* @param request
* The servlet request.
* @param response
* servlet The servlet response.
* @throws ServletException
* If an error occurs that effects the servlet's basic operation.
* @throws IOException
* If an error occurrs with an input or output operation.
*/
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String id = null;
String dsPhysicalLocation = null;
String dsControlGroupType = null;
String user = null;
String pass = null;
MIMETypedStream mimeTypedStream = null;
DisseminationService ds = null;
Timestamp keyTimestamp = null;
Timestamp currentTimestamp = null;
PrintWriter out = null;
ServletOutputStream outStream = null;
String requestURI =
request.getRequestURL().toString() + "?"
+ request.getQueryString();
id = request.getParameter("id").replaceAll("T", " ");
LOG.debug("Datastream tempID=" + id);
LOG.debug("DRS doGet()");
try {
// Check for required id parameter.
if (id == null || id.equalsIgnoreCase("")) {
String message =
"[DatastreamResolverServlet] No datastream ID "
+ "specified in servlet request: "
+ request.getRequestURI();
LOG.error(message);
response
.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response
.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
message);
return;
}
id = id.replaceAll("T", " ").replaceAll("/", "").trim();
// Get in-memory hashtable of mappings from Fedora server.
ds = new DisseminationService();
dsRegistry = DisseminationService.dsRegistry;
DatastreamMediation dm = (DatastreamMediation) dsRegistry.get(id);
if (dm == null) {
StringBuffer entries = new StringBuffer();
Iterator eIter = dsRegistry.keySet().iterator();
while (eIter.hasNext()) {
entries.append("'" + (String) eIter.next() + "' ");
}
throw new IOException("Cannot find datastream in temp registry by key: "
+ id + "\n" + "Reg entries: " + entries.toString());
}
dsPhysicalLocation = dm.dsLocation;
dsControlGroupType = dm.dsControlGroupType;
user = dm.callUsername;
pass = dm.callPassword;
if (LOG.isDebugEnabled()) {
LOG
.debug("**************************** DatastreamResolverServlet dm.dsLocation: "
+ dm.dsLocation);
LOG
.debug("**************************** DatastreamResolverServlet dm.dsControlGroupType: "
+ dm.dsControlGroupType);
LOG
.debug("**************************** DatastreamResolverServlet dm.callUsername: "
+ dm.callUsername);
LOG
.debug("**************************** DatastreamResolverServlet dm.Password: "
+ dm.callPassword);
LOG
.debug("**************************** DatastreamResolverServlet dm.callbackRole: "
+ dm.callbackRole);
LOG
.debug("**************************** DatastreamResolverServlet dm.callbackBasicAuth: "
+ dm.callbackBasicAuth);
LOG
.debug("**************************** DatastreamResolverServlet dm.callBasicAuth: "
+ dm.callBasicAuth);
LOG
.debug("**************************** DatastreamResolverServlet dm.callbackSSl: "
+ dm.callbackSSL);
LOG
.debug("**************************** DatastreamResolverServlet dm.callSSl: "
+ dm.callSSL);
LOG
.debug("**************************** DatastreamResolverServlet non ssl port: "
+ fedoraServerPort);
LOG
.debug("**************************** DatastreamResolverServlet ssl port: "
+ fedoraServerRedirectPort);
}
// DatastreamResolverServlet maps to two distinct servlet mappings
// in fedora web.xml.
// getDS - is used when the backend service is incapable of
// basicAuth or SSL
// getDSAuthenticated - is used when the backend service has
// basicAuth and SSL enabled
// Since both the getDS and getDSAuthenticated servlet targets map
// to the same servlet
// code and the Context used to initialize policy enforcement is
// based on the incoming
// HTTPRequest, the code must provide special handling for requests
// using the getDS
// target. When the incoming URL to DatastreamResolverServlet
// contains the getDS target,
// there are several conditions that must be checked to insure that
// the correct role is
// assigned to the request before policy enforcement occurs.
// 1) if the mapped dsPhysicalLocation of the request is actually a
// callback to the
// Fedora server itself, then assign the role as
// BACKEND_SERVICE_CALL_UNSECURE so
// the basicAuth and SSL constraints will match those of the getDS
// target.
// 2) if the mapped dsPhysicalLocation of the request is actually a
// Managed Content
// or Inline XML Content datastream, then assign the role as
// BACKEND_SERVICE_CALL_UNSECURE so
// the basicAuth and SSL constraints will match the getDS target.
// 3) Otherwise, leave the targetrole unchanged.
if (request.getRequestURI().endsWith("getDS")
&& (ServerUtility.isURLFedoraServer(dsPhysicalLocation)
|| dsControlGroupType.equals("M") || dsControlGroupType
.equals("X"))) {
if (LOG.isDebugEnabled()) {
LOG.debug("*********************** Changed role from: "
+ dm.callbackRole + " to: "
+ BackendPolicies.BACKEND_SERVICE_CALL_UNSECURE);
}
dm.callbackRole = BackendPolicies.BACKEND_SERVICE_CALL_UNSECURE;
}
// If callback is to fedora server itself and callback is over SSL,
// adjust the protocol and port
// on the URL to match settings of Fedora server. This is necessary
// since the SSL settings for the
// backend service may have specified basicAuth=false, but contained
// datastreams that are callbacks
// to the local Fedora server which requires SSL. The version of
// HttpClient currently in use does
// not handle autoredirecting from http to https so it is necessary
// to set the protocol and port
// to the appropriate secure port.
if (dm.callbackRole.equals(BackendPolicies.FEDORA_INTERNAL_CALL)) {
if (dm.callbackSSL) {
dsPhysicalLocation =
dsPhysicalLocation.replaceFirst("http:", "https:");
dsPhysicalLocation =
dsPhysicalLocation
.replaceFirst(fedoraServerPort,
fedoraServerRedirectPort);
if (LOG.isDebugEnabled()) {
LOG
.debug("*********************** DatastreamResolverServlet -- Was Fedora-to-Fedora call -- modified dsPhysicalLocation: "
+ dsPhysicalLocation);
}
}
}
keyTimestamp = Timestamp.valueOf(ds.extractTimestamp(id));
currentTimestamp = new Timestamp(new Date().getTime());
LOG.debug("dsPhysicalLocation=" + dsPhysicalLocation
+ "dsControlGroupType=" + dsControlGroupType);
// Deny mechanism requests that fall outside the specified time
// interval.
// The expiration limit can be adjusted using the Fedora config
// parameter
// named "datastreamMediationLimit" which is in milliseconds.
long diff = currentTimestamp.getTime() - keyTimestamp.getTime();
LOG.debug("Timestamp diff for mechanism's reponse: " + diff
+ " ms.");
if (diff > (long) datastreamMediationLimit) {
out = response.getWriter();
response.setContentType(HTML_CONTENT_TYPE);
out
.println("<br><b>[DatastreamResolverServlet] Error:</b>"
+ "<font color=\"red\"> Deployment has failed to respond "
+ "to the DatastreamResolverServlet within the specified "
+ "time limit of \""
+ datastreamMediationLimit
+ "\""
+ "milliseconds. Datastream access denied.");
LOG.error("Deployment failed to respond to "
+ "DatastreamResolverServlet within time limit of "
+ datastreamMediationLimit);
out.close();
return;
}
if (dm.callbackRole == null) {
throw new AuthzOperationalException("no callbackRole for this ticket");
}
String targetRole = //Authorization.FEDORA_ROLE_KEY + "=" +
dm.callbackRole; // restrict access to role of this
// ticket
String[] targetRoles = {targetRole};
Context context =
ReadOnlyContext.getContext(Constants.HTTP_REQUEST.REST.uri,
request); // , targetRoles);
if (request.getRemoteUser() == null) {
// non-authn: must accept target role of ticket
LOG.debug("DatastreamResolverServlet: unAuthenticated request");
} else {
// authn: check user roles for target role of ticket
/*
* LOG.debug("DatastreamResolverServlet: Authenticated request
* getting user"); String[] roles = null; Principal principal =
* request.getUserPrincipal(); if (principal == null) { // no
* principal to grok roles from!! } else { try { roles =
* ReadOnlyContext.getRoles(principal); } catch (Throwable t) { } }
* if (roles == null) { roles = new String[0]; }
*/
//XXXXXXXXXXXXXXXXXXXXXXxif (contains(roles, targetRole)) {
LOG.debug("DatastreamResolverServlet: user=="
+ request.getRemoteUser());
/*
* if
* (((ExtendedHttpServletRequest)request).isUserInRole(targetRole)) {
* LOG.debug("DatastreamResolverServlet: user has required
* role"); } else { LOG.debug("DatastreamResolverServlet: authZ
* exception in validating user"); throw new
* AuthzDeniedException("wrong user for this ticket"); }
*/
}
if (LOG.isDebugEnabled()) {
LOG.debug("debugging backendService role");
LOG.debug("targetRole=" + targetRole);
int targetRolesLength = targetRoles.length;
LOG.debug("targetRolesLength=" + targetRolesLength);
if (targetRolesLength > 0) {
LOG.debug("targetRoles[0]=" + targetRoles[0]);
}
int nSubjectValues = context.nSubjectValues(targetRole);
LOG.debug("nSubjectValues=" + nSubjectValues);
if (nSubjectValues > 0) {
LOG.debug("context.getSubjectValue(targetRole)="
+ context.getSubjectValue(targetRole));
}
Iterator it = context.subjectAttributes();
while (it.hasNext()) {
String name = (String) it.next();
int n = context.nSubjectValues(name);
switch (n) {
case 0:
LOG.debug("no subject attributes for " + name);
break;
case 1:
String value = context.getSubjectValue(name);
LOG.debug("single subject attributes for " + name
+ "=" + value);
break;
default:
String[] values = context.getSubjectValues(name);
for (String element : values) {
LOG
.debug("another subject attribute from context "
+ name + "=" + element);
}
}
}
it = context.environmentAttributes();
while (it.hasNext()) {
String name = (String) it.next();
String value = context.getEnvironmentValue(name);
LOG.debug("another environment attribute from context "
+ name + "=" + value);
}
}
/*
* // Enforcement of Backend Security is temporarily disabled
* pending refactoring. // LOG.debug("DatastreamResolverServlet:
* about to do final authZ check"); Authorization authorization =
* (Authorization) s_server
* .getModule("fedora.server.security.Authorization");
* authorization.enforceResolveDatastream(context, keyTimestamp);
* LOG.debug("DatastreamResolverServlet: final authZ check
* suceeded.....");
*/
if (dsControlGroupType.equalsIgnoreCase("E")) {
// testing to see what's in request header that might be of
// interest
if (LOG.isDebugEnabled()) {
for (Enumeration e = request.getHeaderNames(); e
.hasMoreElements();) {
String name = (String) e.nextElement();
Enumeration headerValues = request.getHeaders(name);
StringBuffer sb = new StringBuffer();
while (headerValues.hasMoreElements()) {
sb.append((String) headerValues.nextElement());
}
String value = sb.toString();
LOG
.debug("DATASTREAMRESOLVERSERVLET REQUEST HEADER CONTAINED: "
+ name + " : " + value);
}
}
// Datastream is ReferencedExternalContent so dsLocation is a
// URL string
ExternalContentManager externalContentManager =
(ExternalContentManager) s_server
.getModule("fedora.server.storage.ExternalContentManager");
ContentManagerParams params = new ContentManagerParams(dsPhysicalLocation);
params.setContext(context);
mimeTypedStream = externalContentManager.getExternalContent(params);
// had substituted context:
// ReadOnlyContext.getContext(Constants.HTTP_REQUEST.REST.uri,
// request));
outStream = response.getOutputStream();
response.setContentType(mimeTypedStream.MIMEType);
Property[] headerArray = mimeTypedStream.header;
if (headerArray != null) {
for (int i = 0; i < headerArray.length; i++) {
if (headerArray[i].name != null
&& !headerArray[i].name
.equalsIgnoreCase("content-type")) {
response.addHeader(headerArray[i].name,
headerArray[i].value);
LOG
.debug("THIS WAS ADDED TO DATASTREAMRESOLVERSERVLET RESPONSE HEADER FROM ORIGINATING PROVIDER "
+ headerArray[i].name
+ " : "
+ headerArray[i].value);
}
}
}
int byteStream = 0;
byte[] buffer = new byte[255];
while ((byteStream = mimeTypedStream.getStream().read(buffer)) != -1) {
outStream.write(buffer, 0, byteStream);
}
buffer = null;
outStream.flush();
mimeTypedStream.close();
} else if (dsControlGroupType.equalsIgnoreCase("M")
|| dsControlGroupType.equalsIgnoreCase("X")) {
// Datastream is either XMLMetadata or ManagedContent so
// dsLocation
// is in the form of an internal Fedora ID using the syntax:
// PID+DSID+DSVersID; parse the ID and get the datastream
// content.
String PID = null;
String dsVersionID = null;
String dsID = null;
String[] s = dsPhysicalLocation.split("\\+");
if (s.length != 3) {
String message =
"[DatastreamResolverServlet] The "
+ "internal Fedora datastream id: \""
+ dsPhysicalLocation + "\" is invalid.";
LOG.error(message);
throw new ServletException(message);
}
PID = s[0];
dsID = s[1];
dsVersionID = s[2];
LOG.debug("PID=" + PID + ", dsID=" + dsID + ", dsVersionID="
+ dsVersionID);
DOReader doReader =
m_manager.getReader(Server.USE_DEFINITIVE_STORE,
context,
PID);
Datastream d =
(Datastream) doReader.getDatastream(dsID, dsVersionID);
LOG.debug("Got datastream: " + d.DatastreamID);
InputStream is = d.getContentStream();
int bytestream = 0;
response.setContentType(d.DSMIME);
outStream = response.getOutputStream();
byte[] buffer = new byte[255];
while ((bytestream = is.read(buffer)) != -1) {
outStream.write(buffer, 0, bytestream);
}
buffer = null;
is.close();
} else {
out = response.getWriter();
response.setContentType(HTML_CONTENT_TYPE);
out
.println("<br>[DatastreamResolverServlet] Unknown "
+ "dsControlGroupType: " + dsControlGroupType
+ "</br>");
LOG.error("Unknown dsControlGroupType: " + dsControlGroupType);
}
} catch (AuthzException ae) {
LOG.error("Authorization failure resolving datastream"
+ " (actionLabel=" + ACTION_LABEL + ")", ae);
throw RootException.getServletException(ae,
request,
ACTION_LABEL,
new String[0]);
} catch (Throwable th) {
LOG.error("Error resolving datastream", th);
String message =
"[DatastreamResolverServlet] returned an error. The "
+ "underlying error was a \""
+ th.getClass().getName() + " The message was \""
+ th.getMessage() + "\". ";
throw new ServletException(message);
} finally {
if (out != null) {
out.close();
}
if (outStream != null) {
outStream.close();
}
dsRegistry.remove(id);
}
}
// Clean up resources
public void destroy() {
}
}