/**********************************************************************************
* $URL: https://source.sakaiproject.org/svn/access/trunk/access-impl/impl/src/java/org/sakaiproject/access/tool/AccessServlet.java $
* $Id: AccessServlet.java 125715 2013-06-13 15:51:41Z ottenhoff@longsight.com $
***********************************************************************************
*
* Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008, 2009 The Sakai Foundation
*
* Licensed under the Educational Community 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.opensource.org/licenses/ECL-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.sakaiproject.access.tool;
import java.io.IOException;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Properties;
import java.util.Vector;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.sakaiproject.authz.api.SecurityAdvisor;
import org.sakaiproject.authz.cover.SecurityService;
import org.sakaiproject.cheftool.VmServlet;
import org.sakaiproject.entity.api.Entity;
import org.sakaiproject.entity.api.EntityAccessOverloadException;
import org.sakaiproject.entity.api.EntityCopyrightException;
import org.sakaiproject.entity.api.EntityNotDefinedException;
import org.sakaiproject.entity.api.EntityPermissionException;
import org.sakaiproject.entity.api.EntityProducer;
import org.sakaiproject.entity.api.HttpAccess;
import org.sakaiproject.entity.api.Reference;
import org.sakaiproject.entity.api.ResourceProperties;
import org.sakaiproject.entity.cover.EntityManager;
import org.sakaiproject.tool.api.ActiveTool;
import org.sakaiproject.tool.api.Session;
import org.sakaiproject.tool.api.Tool;
import org.sakaiproject.tool.api.ToolException;
import org.sakaiproject.tool.cover.ActiveToolManager;
import org.sakaiproject.tool.cover.SessionManager;
import org.sakaiproject.util.BaseResourceProperties;
import org.sakaiproject.util.BasicAuth;
import org.sakaiproject.util.ParameterParser;
import org.sakaiproject.util.ResourceLoader;
import org.sakaiproject.util.Validator;
import org.sakaiproject.util.Web;
/**
* <p>
* Access is a servlet that provides a portal to entity access by URL for Sakai.<br />
* The servlet takes the requests and dispatches to the appropriate EntityProducer for the response.<br />
* Any error handling is done here.<br />
* If the user has not yet logged in and need to for permission, the login process is handled here, too.
* </p>
*
* @author Sakai Software Development Team
*/
public class AccessServlet extends VmServlet
{
/** Our log (commons). */
private static Log M_log = LogFactory.getLog(AccessServlet.class);
/** Resource bundle using current language locale */
protected static ResourceLoader rb = new ResourceLoader("access");
/** stream content requests if true, read all into memory and send if false. */
protected static final boolean STREAM_CONTENT = true;
/** The chunk size used when streaming (100k). */
protected static final int STREAM_BUFFER_SIZE = 102400;
/** delimiter for form multiple values */
protected static final String FORM_VALUE_DELIMETER = "^";
/** set to true when init'ed. */
protected boolean m_ready = false;
/** copyright path -- MUST have same value as ResourcesAction.COPYRIGHT_PATH */
protected static final String COPYRIGHT_PATH = Entity.SEPARATOR + "copyright";
/** Path used when forcing the user to accept the copyright agreement . */
protected static final String COPYRIGHT_REQUIRE = Entity.SEPARATOR + "require";
/** Path used when the user has accepted the copyright agreement . */
protected static final String COPYRIGHT_ACCEPT = Entity.SEPARATOR + "accept";
/** Ref accepted, request parameter for COPYRIGHT_ACCEPT request. */
protected static final String COPYRIGHT_ACCEPT_REF = "ref";
/** Return URL, request parameter for COPYRIGHT_ACCEPT request. */
protected static final String COPYRIGHT_ACCEPT_URL = "url";
/** Session attribute holding copyright-accepted references (a collection of Strings). */
protected static final String COPYRIGHT_ACCEPTED_REFS_ATTR = "Access.Copyright.Accepted";
protected BasicAuth basicAuth = null;
/** init thread - so we don't wait in the actual init() call */
public class AccessServletInit extends Thread
{
/**
* construct and start the init activity
*/
public AccessServletInit()
{
m_ready = false;
start();
}
/**
* run the init
*/
public void run()
{
m_ready = true;
}
}
/**
* initialize the AccessServlet servlet
*
* @param config
* the servlet config parameter
* @exception ServletException
* in case of difficulties
*/
public void init(ServletConfig config) throws ServletException
{
super.init(config);
startInit();
basicAuth = new BasicAuth();
basicAuth.init();
}
/**
* Start the initialization process
*/
public void startInit()
{
new AccessServletInit();
}
/**
* respond to an HTTP GET request
*
* @param req
* HttpServletRequest object with the client request
* @param res
* HttpServletResponse object back to the client
* @exception ServletException
* in case of difficulties
* @exception IOException
* in case of difficulties
*/
public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException
{
// process any login that might be present
basicAuth.doLogin(req);
// catch the login helper requests
String option = req.getPathInfo();
String[] parts = option.split("/");
if ((parts.length == 2) && ((parts[1].equals("login"))))
{
doLogin(req, res, null);
}
else
{
dispatch(req, res);
}
}
/**
* respond to an HTTP POST request; only to handle the login process
*
* @param req
* HttpServletRequest object with the client request
* @param res
* HttpServletResponse object back to the client
* @exception ServletException
* in case of difficulties
* @exception IOException
* in case of difficulties
*/
public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException
{
// process any login that might be present
basicAuth.doLogin(req);
// catch the login helper posts
String option = req.getPathInfo();
String[] parts = option.split("/");
if ((parts.length == 2) && ((parts[1].equals("login"))))
{
doLogin(req, res, null);
}
else
{
sendError(res, HttpServletResponse.SC_NOT_FOUND);
}
}
/**
* handle get and post communication from the user
*
* @param req
* HttpServletRequest object with the client request
* @param res
* HttpServletResponse object back to the client
*/
public void dispatch(HttpServletRequest req, HttpServletResponse res) throws ServletException
{
ParameterParser params = (ParameterParser) req.getAttribute(ATTR_PARAMS);
// get the path info
String path = params.getPath();
if (path == null) path = "";
if (!m_ready)
{
sendError(res, HttpServletResponse.SC_SERVICE_UNAVAILABLE);
return;
}
// send the sample copyright screen
if (COPYRIGHT_PATH.equals(path))
{
respondCopyrightAlertDemo(req, res);
return;
}
// send the real copyright screen for some entity (encoded in the request parameter)
if (COPYRIGHT_REQUIRE.equals(path))
{
String acceptedRef = req.getParameter(COPYRIGHT_ACCEPT_REF);
String returnPath = req.getParameter(COPYRIGHT_ACCEPT_URL);
Reference aRef = EntityManager.newReference(acceptedRef);
// get the properties - but use a security advisor to avoid needing end-user permission to the resource
SecurityService.pushAdvisor(new SecurityAdvisor()
{
public SecurityAdvice isAllowed(String userId, String function, String reference)
{
return SecurityAdvice.ALLOWED;
}
});
ResourceProperties props = aRef.getProperties();
SecurityService.popAdvisor();
// send the copyright agreement interface
if (props == null)
{
sendError(res, HttpServletResponse.SC_NOT_FOUND);
}
setVmReference("validator", new Validator(), req);
setVmReference("props", props, req);
setVmReference("tlang", rb, req);
String acceptPath = Web.returnUrl(req, COPYRIGHT_ACCEPT + "?" + COPYRIGHT_ACCEPT_REF + "=" + Validator.escapeUrl(aRef.getReference()) + "&"
+ COPYRIGHT_ACCEPT_URL + "=" + Validator.escapeUrl(returnPath));
setVmReference("accept", acceptPath, req);
res.setContentType("text/html; charset=UTF-8");
includeVm("vm/access/copyrightAlert.vm", req, res);
return;
}
// make sure we have a collection for accepted copyright agreements
Collection accepted = (Collection) SessionManager.getCurrentSession().getAttribute(COPYRIGHT_ACCEPTED_REFS_ATTR);
if (accepted == null)
{
accepted = new Vector();
SessionManager.getCurrentSession().setAttribute(COPYRIGHT_ACCEPTED_REFS_ATTR, accepted);
}
// for accepted copyright, mark it and redirect to the entity's access URL
if (COPYRIGHT_ACCEPT.equals(path))
{
String acceptedRef = req.getParameter(COPYRIGHT_ACCEPT_REF);
Reference aRef = EntityManager.newReference(acceptedRef);
// save this with the session's other accepted refs
accepted.add(aRef.getReference());
// redirect to the original URL
String returnPath = Validator.escapeUrl( req.getParameter(COPYRIGHT_ACCEPT_URL) );
try
{
res.sendRedirect(Web.returnUrl(req, returnPath));
}
catch (IOException e)
{
sendError(res, HttpServletResponse.SC_NOT_FOUND);
}
return;
}
// pre-process the path
String origPath = path;
path = preProcessPath(path, req);
// what is being requested?
Reference ref = EntityManager.newReference(path);
// get the incoming information
AccessServletInfo info = newInfo(req);
// let the entity producer handle it
try
{
// make sure we have a valid reference with an entity producer we can talk to
EntityProducer service = ref.getEntityProducer();
if (service == null) throw new EntityNotDefinedException(ref.getReference());
// get the producer's HttpAccess helper, it might not support one
HttpAccess access = service.getHttpAccess();
if (access == null) throw new EntityNotDefinedException(ref.getReference());
// let the helper do the work
access.handleAccess(req, res, ref, accepted);
}
catch (EntityNotDefinedException e)
{
// the request was not valid in some way
M_log.error("dispatch(): ref: " + ref.getReference() + e);
sendError(res, HttpServletResponse.SC_NOT_FOUND);
return;
}
catch (EntityPermissionException e)
{
// the end user does not have permission - offer a login if there is no user id yet established
// if not permitted, and the user is the anon user, let them login
if (SessionManager.getCurrentSessionUserId() == null)
{
try {
doLogin(req, res, origPath);
} catch ( IOException ioex ) {}
return;
}
// otherwise reject the request
M_log.error("dispatch(): ref: " + ref.getReference() + e);
sendError(res, HttpServletResponse.SC_FORBIDDEN);
}
catch (EntityAccessOverloadException e)
{
M_log.info("dispatch(): ref: " + ref.getReference() + e);
sendError(res, HttpServletResponse.SC_SERVICE_UNAVAILABLE);
}
catch (EntityCopyrightException e)
{
// redirect to the copyright agreement interface for this entity
try
{
// TODO: send back using a form of the request URL, encoding the real reference, and the requested reference
// Note: refs / requests with servlet parameters (?x=y...) are NOT supported -ggolden
String redirPath = COPYRIGHT_REQUIRE + "?" + COPYRIGHT_ACCEPT_REF + "=" + Validator.escapeUrl(e.getReference()) + "&" + COPYRIGHT_ACCEPT_URL
+ "=" + Validator.escapeUrl(req.getPathInfo());
res.sendRedirect(Web.returnUrl(req, redirPath));
}
catch (IOException ee)
{
}
return;
}
catch (Throwable e)
{
M_log.warn("dispatch(): exception: ", e);
sendError(res, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
finally
{
// log
if (M_log.isDebugEnabled())
M_log.debug("from:" + req.getRemoteAddr() + " path:" + params.getPath() + " options: " + info.optionsString()
+ " time: " + info.getElapsedTime());
}
}
/**
* Make any changes needed to the path before final "ref" processing.
*
* @param path
* The path from the request.
* @req The request object.
* @return The path to use to make the Reference for further processing.
*/
protected String preProcessPath(String path, HttpServletRequest req)
{
return path;
}
/**
* Make the Sample Copyright Alert response.
*
* @param req
* HttpServletRequest object with the client request.
* @param res
* HttpServletResponse object back to the client.
*/
protected void respondCopyrightAlertDemo(HttpServletRequest req, HttpServletResponse res) throws ServletException
{
// the context wraps our real vm attribute set
ResourceProperties props = new BaseResourceProperties();
setVmReference("props", props, req);
setVmReference("validator", new Validator(), req);
setVmReference("sample", Boolean.TRUE.toString(), req);
setVmReference("tlang", rb, req);
res.setContentType("text/html; charset=UTF-8");
includeVm("vm/access/copyrightAlert.vm", req, res);
}
/**
* Make a redirect to the login url.
*
* @param req
* HttpServletRequest object with the client request.
* @param res
* HttpServletResponse object back to the client.
* @param path
* The current request path, set ONLY if we want this to be where to redirect the user after successfull login
* @throws IOException
*/
protected void doLogin(HttpServletRequest req, HttpServletResponse res, String path) throws ToolException, IOException
{
// if basic auth is valid do that
if ( basicAuth.doAuth(req,res) ) {
//System.err.println("BASIC Auth Request Sent to the Browser ");
return;
}
// if there is a Range: header for partial content and we haven't done basic auth, refuse the request (SAK-23678)
if (req.getHeader("Range") != null) {
sendError(res, HttpServletResponse.SC_FORBIDDEN);
return;
}
// get the Sakai session
Session session = SessionManager.getCurrentSession();
// set the return path for after login if needed (Note: in session, not tool session, special for Login helper)
if (path != null)
{
// where to go after
session.setAttribute(Tool.HELPER_DONE_URL, Web.returnUrl(req, Validator.escapeUrl(path)));
}
// check that we have a return path set; might have been done earlier
if (session.getAttribute(Tool.HELPER_DONE_URL) == null)
{
M_log.warn("doLogin - proceeding with null HELPER_DONE_URL");
}
// map the request to the helper, leaving the path after ".../options" for the helper
ActiveTool tool = ActiveToolManager.getActiveTool("sakai.login");
String context = req.getContextPath() + req.getServletPath() + "/login";
tool.help(req, res, context, "/login");
}
/** create the info */
protected AccessServletInfo newInfo(HttpServletRequest req)
{
return new AccessServletInfo(req);
}
protected void sendError(HttpServletResponse res, int code)
{
try
{
res.sendError(code);
}
catch (Throwable t)
{
M_log.warn("sendError: " + t);
}
}
public class AccessServletInfo
{
// elapsed time start
protected long m_startTime = System.currentTimeMillis();
public long getStartTime()
{
return m_startTime;
}
public long getElapsedTime()
{
return System.currentTimeMillis() - m_startTime;
}
// all properties from the request
protected Properties m_options = null;
/** construct from the req */
public AccessServletInfo(HttpServletRequest req)
{
m_options = new Properties();
String type = req.getContentType();
Enumeration e = req.getParameterNames();
while (e.hasMoreElements())
{
String key = (String) e.nextElement();
String[] values = req.getParameterValues(key);
if (values.length == 1)
{
m_options.put(key, values[0]);
}
else
{
StringBuilder buf = new StringBuilder();
for (int i = 0; i < values.length; i++)
{
buf.append(values[i] + FORM_VALUE_DELIMETER);
}
m_options.put(key, buf.toString());
}
}
}
/** return the m_options as a string - obscure any "password" fields */
public String optionsString()
{
StringBuilder buf = new StringBuilder(1024);
Enumeration e = m_options.keys();
while (e.hasMoreElements())
{
String key = (String) e.nextElement();
Object o = m_options.getProperty(key);
if (o instanceof String)
{
buf.append(key);
buf.append("=");
if (key.equals("password"))
{
buf.append("*****");
}
else
{
buf.append(o.toString());
}
buf.append("&");
}
}
return buf.toString();
}
}
/**
* A simple SecurityAdviser that can be used to override permissions on one reference string for one user for one function.
*/
public class SimpleSecurityAdvisor implements SecurityAdvisor
{
protected String m_userId;
protected String m_function;
protected String m_reference;
public SimpleSecurityAdvisor(String userId, String function, String reference)
{
m_userId = userId;
m_function = function;
m_reference = reference;
}
public SecurityAdvice isAllowed(String userId, String function, String reference)
{
SecurityAdvice rv = SecurityAdvice.PASS;
if (m_userId.equals(userId) && m_function.equals(function) && m_reference.equals(reference))
{
rv = SecurityAdvice.ALLOWED;
}
return rv;
}
}
}