/* ========================================================================== *
* Copyright (C) 2004-2005 Pier Fumagalli <http://www.betaversion.org/~pier/> *
* All rights reserved. *
* ========================================================================== *
* *
* Licensed 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 com.adito.vfs.webdav;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts.Globals;
import org.apache.struts.action.ActionMessages;
import org.apache.struts.util.MessageResources;
import com.adito.boot.SystemProperties;
import com.adito.core.BundleActionMessage;
import com.adito.core.CoreException;
import com.adito.core.CoreUtil;
import com.adito.policyframework.LaunchSession;
import com.adito.security.AuthenticationModuleManager;
import com.adito.security.AuthenticationScheme;
import com.adito.security.Constants;
import com.adito.security.LogonControllerFactory;
import com.adito.security.SessionInfo;
import com.adito.security.WebDAVAuthenticationModule;
import com.adito.vfs.VFSRepository;
import com.adito.vfs.VFSResource;
/**
* A servlet capable of processing very simple <a
* href="http://www.rfc-editor.org/rfc/rfc2518.txt">WebDAV</a> requests.
* <p>
* This code was originally based on Pier Fumagallis WebDAV servlet, but as
* Adito has developed, much of the original code has been replaced.
* <p>
* The servlet is now fully integrated into Adito and its <i>Virtual File
* System</i>, so any mount can be exposed via WebDAV.
*
* @author <a href="http://localhost/">3SP</a>
* @author <a href="http://www.betaversion.org/~pier/">Pier Fumagalli</a>
*/
public class DAVServlet implements Servlet, DAVListener {
final static Log log = LogFactory.getLog(DAVServlet.class);
private static String PROCESSOR_ATTR = "davServlet.processor";
private static String SESSION_INVALIDATE_LISTENER_ATTR = "davServlet.sessionInvalidateListener";
private ServletContext context = null;
private ServletConfig config = null;
private static List<DAVProcessor> processors = new ArrayList<DAVProcessor>();
/*
* (non-Javadoc)
*
* @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
*/
public void init(ServletConfig config) throws ServletException {
/* Remember the configuration instance */
this.context = config.getServletContext();
this.config = config;
}
/*
* (non-Javadoc)
*
* @see javax.servlet.Servlet#destroy()
*/
public void destroy() {
for (DAVProcessor processor : processors) {
processor.getRepository().removeListener(this);
}
}
/*
* (non-Javadoc)
*
* @see javax.servlet.Servlet#getServletConfig()
*/
public ServletConfig getServletConfig() {
return (this.config);
}
/*
* (non-Javadoc)
*
* @see javax.servlet.Servlet#getServletInfo()
*/
public String getServletInfo() {
return "WebDAV";
}
/**
* Get the {@link DAVProcessor} from the requests session.
*
* @param req request
* @return processor
* @throws CoreException on any error
* @throws Exception on any error
*/
public static DAVProcessor getDAVProcessor(HttpServletRequest req) throws CoreException, Exception {
return getDAVProcessor(req.getSession());
}
/**
* Get the {@link DAVProcessor} from the session.
*
* @param session sesison
* @return processor
* @throws CoreException on any error
* @throws Exception on any error
*/
public static DAVProcessor getDAVProcessor(HttpSession session) throws Exception {
DAVProcessor processor = (DAVProcessor) session.getAttribute(PROCESSOR_ATTR);
if (processor == null) {
VFSRepository repository = VFSRepository.getRepository(session);
processor = new DAVProcessor(repository);
processors.add(processor);
session.setAttribute(PROCESSOR_ATTR, processor);
session.setAttribute(SESSION_INVALIDATE_LISTENER_ATTR, new SessionInvalidateListener(processor));
if (log.isInfoEnabled())
log.info("Initialized repository");
}
return processor;
}
/**
* Get the resource give its path and various session related attributes.
*
* @param launchSession launch sesion
* @param request request
* @param response response
* @param path resource path
* @return resource object
* @throws DAVBundleActionMessageException on any VFS or DAV related errors
* @throws Exception on any other error
*/
public static VFSResource getDAVResource(LaunchSession launchSession, HttpServletRequest request, HttpServletResponse response,
String path) throws DAVBundleActionMessageException, Exception {
VFSResource res = null;
try {
DAVProcessor processor = getDAVProcessor(request);
DAVTransaction transaction = new DAVTransaction(request, response);
res = processor.getRepository().getResource(launchSession, path, transaction.getCredentials());
// res.start(transaction);
res.verifyAccess();
} catch (DAVAuthenticationRequiredException e) {
DAVServlet.sendAuthorizationError(request, response, e.getHttpRealm());
throw e;
}
return res;
}
/*
* (non-Javadoc)
*
* @see javax.servlet.Servlet#service(javax.servlet.ServletRequest,
* javax.servlet.ServletResponse)
*/
public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
debugRequest(req);
DAVTransaction transaction = null;
try {
transaction = new DAVTransaction(req, res);
DAVProcessor processor = null;
try {
processor = DAVServlet.getDAVProcessor(req);
} catch (CoreException e) {
ActionMessages mesgs = (ActionMessages) request.getAttribute(Globals.ERROR_KEY);
if (mesgs == null) {
mesgs = new ActionMessages();
request.setAttribute(Globals.ERROR_KEY, mesgs);
}
mesgs.add(Globals.MESSAGE_KEY, e.getBundleActionMessage());
return;
} catch (Exception e1) {
throw new IOException(e1.getMessage());
}
/**
* LDP - JB I have removed the authentication code here and instead
* placed in DAVTransaction. This allows the transaction to
* correctly obtain sessionInfo through WEBDav.
*/
/* Mark our presence */
res.setHeader("Server", this.context.getServerInfo() + " WebDAV");
res.setHeader("MS-Author-Via", "DAV");
res.setHeader("DAV", "1");
/*
* Get the current SessionInfo if possible, but do not request
* authentication yet This is because the VFSResource that we want
* is in a mount that does not require Adito authentication
* (required for some ActiveX and Applet extensions).
*
* SessionInfo will be null if session can yet be determined
*/
if (!transaction.attemptToAuthorize()) {
return;
}
SessionInfo sessionInfo = transaction.getSessionInfo();
// Timeout block is not needed if we have no session
int timeoutId = sessionInfo == null ? -1 : LogonControllerFactory.getInstance().addSessionTimeoutBlock(
transaction.getSessionInfo().getHttpSession(), "DAV Transaction");
;
try {
processor.process(transaction);
} catch (DAVAuthenticationRequiredException dare) {
/*
* If the session is temporary, then we are probably dealing
* with a client that doesn't support cookies. This means that
* secondary authentication that may be required for some mounts
* won't work as we cannot have two different sets of
* credentials without session tracking
*/
if (sessionInfo != null && sessionInfo.isTemporary()) {
throw new IOException("Mount requires further authentication. This cannot work "
+ "on WebDAV clients that do not support cookies.");
} else {
throw dare;
}
} finally {
if (timeoutId != -1)
LogonControllerFactory.getInstance().removeSessionTimeoutBlock(transaction.getSessionInfo().getHttpSession(),
timeoutId);
}
} catch (DAVRedirection redir) {
String redirPath = "/fs" + redir.getLocation().getFullPath();
req.getRequestDispatcher(redirPath).forward(req, res);
} catch (DAVAuthenticationRequiredException e) {
// We need to be able to authenticate the Adito session was
// well
// sendAuthorizationError(req, res, e.getMount().getMountString());
sendAuthorizationError(req, res, e.getHttpRealm());
} catch (DAVBundleActionMessageException ex) {
log.error("Network Places Request Failed: " + req.getPathInfo(), ex);
BundleActionMessage bam = ex.getBundleActionMessage();
MessageResources mr = CoreUtil.getMessageResources(req.getSession(), bam.getBundle());
// TODO locale
String val = mr == null ? null : mr.getMessage(bam.getKey());
res.sendError(DAVStatus.SC_INTERNAL_SERVER_ERROR, val == null ? (ex.getMessage() == null ? "No message supplied." : ex
.getMessage()) : val);
} catch (DAVException ex) {
res.setStatus(ex.getStatus());
} catch (LockedException ex) {
res.sendError(DAVStatus.SC_LOCKED, ex.getMessage());
} catch (Throwable t) {
log.error("Network Places Request Failed: " + req.getPathInfo(), t);
res.sendError(DAVStatus.SC_INTERNAL_SERVER_ERROR, t.getMessage() == null ? "<null>" : t.getMessage());
} finally {
if (transaction != null && transaction.getSessionInfo() != null && transaction.getSessionInfo().isTemporary()) {
transaction.getSessionInfo().getHttpSession().invalidate();
}
}
}
private void debugRequest(HttpServletRequest req) {
if (SystemProperties.get("adito.webdav.debug", "false").equalsIgnoreCase("true")) {
for (Enumeration e = req.getHeaderNames(); e.hasMoreElements();) {
String header = (String) e.nextElement();
for (Enumeration e2 = req.getHeaders(header); e2.hasMoreElements();) {
log.info(header + ": " + (String) e2.nextElement());
}
}
}
if (log.isDebugEnabled())
log.debug("Processing " + req.getMethod() + " " + req.getPathInfo());
}
/**
* Add the headers required for the browser to popup an authentication
* dialog for a specified realm, and send the
* {@link HttpServletResponse.SC_UNAUTHORIZED} HTTP response code.
*
* @param request request
* @param response response
* @param realm realm.
* @throws IOException
*/
public static void sendAuthorizationError(HttpServletRequest request, HttpServletResponse response, String realm)
throws IOException {
/*
* If this is for the default realm (i.e Adito Authentication, we
* need to set up an authentication scheme
*/
if (realm.equals(WebDAVAuthenticationModule.DEFAULT_REALM)) {
configureAuthenticationScheme(request, response);
}
// Configure the response
if (log.isDebugEnabled())
log.debug("Sending auth request for realm " + realm);
response.setHeader("WWW-Authenticate", "Basic realm=\"" + realm + "\"");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
request.getSession().setAttribute(DAVTransaction.ATTR_EXPECTING_REALM_AUTHENTICATION, realm);
}
protected static AuthenticationScheme configureAuthenticationScheme(HttpServletRequest request, HttpServletResponse response)
throws IOException {
AuthenticationScheme seq = AuthenticationModuleManager.getInstance().getSchemeForAuthenticationModuleInUse(
WebDAVAuthenticationModule.MODULE_NAME);
if (seq == null || !seq.getEnabled()) {
log
.error("User cannot authenticate via WebDAV using only HTTP BASIC authentication as the current policy does not allow this.");
response
.sendError(DAVStatus.SC_FORBIDDEN,
"You cannot authenticate via WebDAV using only HTTP BASIC authentication as the current policy does not allow this.");
return seq;
}
seq.addModule(WebDAVAuthenticationModule.MODULE_NAME);
try {
seq.init(request.getSession());
} catch (Exception e) {
IOException ioe = new IOException("Failed to authentication scheme.");
ioe.initCause(e);
throw ioe;
}
seq.nextAuthenticationModule();
request.getSession().setAttribute(Constants.AUTH_SENT, Boolean.TRUE);
request.getSession().setAttribute(Constants.AUTH_SESSION, seq);
return seq;
}
/**
* <p>
* Receive notification of an event occurred in a specific
* {@link VFSRepository}.
* </p>
*/
public void notify(VFSResource resource, int event) {
String message = "Unknown event";
switch (event) {
case DAVListener.COLLECTION_CREATED:
message = "Collection created";
break;
case DAVListener.COLLECTION_REMOVED:
message = "Collection removed";
break;
case DAVListener.RESOURCE_CREATED:
message = "Resource created";
break;
case DAVListener.RESOURCE_REMOVED:
message = "Resource removed";
break;
case DAVListener.RESOURCE_MODIFIED:
message = "Resource modified";
break;
}
if (log.isDebugEnabled())
log.debug(message + ": \"" + resource.getRelativePath() + "\"");
}
static class SessionInvalidateListener implements HttpSessionBindingListener {
private DAVProcessor processor;
/**
*
*/
SessionInvalidateListener(DAVProcessor processor) {
this.processor = processor;
}
/*
* (non-Javadoc)
*
* @see javax.servlet.http.HttpSessionBindingListener#valueBound(javax.servlet.http.HttpSessionBindingEvent)
*/
public void valueBound(HttpSessionBindingEvent arg0) {
}
/*
* (non-Javadoc)
*
* @see javax.servlet.http.HttpSessionBindingListener#valueUnbound(javax.servlet.http.HttpSessionBindingEvent)
*/
public void valueUnbound(HttpSessionBindingEvent arg0) {
processors.remove(processor);
// processor.getRepository().removeListener(DAVServlet.this);
}
}
}