Java tutorial
/* ========================================================================== * * 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.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.net.URI; import java.net.URISyntaxException; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.maverick.crypto.encoders.Base64; import com.adito.boot.HttpConstants; import com.adito.boot.SystemProperties; import com.adito.boot.Util; import com.adito.core.ServletRequestAdapter; import com.adito.core.ServletResponseAdapter; import com.adito.core.UserDatabaseManager; import com.adito.policyframework.LaunchSession; import com.adito.policyframework.LaunchSessionFactory; import com.adito.properties.Property; import com.adito.properties.impl.systemconfig.SystemConfigKey; import com.adito.security.AccountLockedException; import com.adito.security.AuthenticationModuleManager; import com.adito.security.AuthenticationScheme; import com.adito.security.Constants; import com.adito.security.DefaultAuthenticationScheme; import com.adito.security.InvalidLoginCredentialsException; import com.adito.security.LogonController; import com.adito.security.LogonControllerFactory; import com.adito.security.PasswordCredentials; import com.adito.security.SessionInfo; import com.adito.security.SystemDatabaseFactory; import com.adito.security.User; import com.adito.security.UserNotFoundException; import com.adito.security.WebDAVAuthenticationModule; import com.adito.security.actions.LogonAction; import com.adito.vfs.VFSResource; /** * <p> * A simple wrapper isolating the Java Servlet API from this <a * href="http://www.rfc-editor.org/rfc/rfc2518.txt">WebDAV</a> implementation. * </p> * * @author <a href="http://www.betaversion.org/~pier/">Pier Fumagalli</a> */ public class DAVTransaction { private static Log log = LogFactory.getLog(DAVTransaction.class); public final static String ATTR_EXPECTING_REALM_AUTHENTICATION = "expectingRealmAuth"; public final static String ATTR_DEREGISTER_SUB_AUTHS = "deregisterSubAuths"; public final static String ATTR_AUTH_ATTEMPTS = "authAttempts"; /** * <p> * The identifyication of the <code>infinity</code> value in the * <code>Depth</code> header. * </p> */ public static final int INFINITY = Integer.MAX_VALUE; /** * <p> * The nested {@link HttpServletRequest}. * </p> */ private HttpServletRequest req = null; /** * <p> * The nested {@link HttpServletResponse}. * </p> */ private HttpServletResponse res = null; /** * <p> * The {@link URI} associated with the base of the repository. * </p> */ private URI base = null; /** * <p> * The path for this transaction. contains user etc. * </p> */ private String path; /** * <p> * A cache of resources for the life of this transaction */ private Map resourceCache; /** * The session */ private SessionInfo sessionInfo; /** * <p> * The current credentials object being used for authentication to the * resources */ // private DAVCredentials currentCredentials; /* ====================================================================== */ /* Constructors */ /* ====================================================================== */ /** * <p> * Create a new {@link DAVTransaction} instance. * </p> * * @throws URISyntaxException */ public DAVTransaction(ServletRequest request, ServletResponse response) // throws ServletException, DAVAuthenticationRequiredException { throws ServletException, URISyntaxException { if (request == null) throw new NullPointerException("Null request"); if (response == null) throw new NullPointerException("Null response"); this.req = (HttpServletRequest) request; this.res = (HttpServletResponse) response; this.resourceCache = new HashMap(); /* * First see if the launch ID has been provided as a parameter. If it * has we can just get the resource session directly. This should happen * for web folders that are first launched from an active user session * or from a file download from the network place HTML file browser. */ String launchId = request.getParameter(LaunchSession.LAUNCH_ID); if (launchId != null) { LaunchSession launchSession = LaunchSessionFactory.getInstance().getLaunchSession(launchId); if (launchSession != null) { sessionInfo = launchSession.getSession(); LogonControllerFactory.getInstance().addCookies( new ServletRequestAdapter((HttpServletRequest) request), new ServletResponseAdapter((HttpServletResponse) response), launchSession.getSession().getLogonTicket(), launchSession.getSession()); sessionInfo.access(); } else if (log.isDebugEnabled()) log.debug("Could not locate session using ticket"); } sessionInfo = LogonControllerFactory.getInstance().getSessionInfo(req); configureFromRequest(); } public void putCachedResource(VFSResource resource) { String key = DAVUtilities.concatenatePaths(resource.getMount().getMountString(), resource.getRelativePath()); resourceCache.put(key, resource); } public VFSResource getCachedResource(String fullPath) { return (VFSResource) resourceCache.get(fullPath); } public boolean attemptToAuthorize() throws IOException, UserNotFoundException, Exception { if (!verifyIp()) { return false; } String expectingRealm = (String) req.getSession().getAttribute(ATTR_EXPECTING_REALM_AUTHENTICATION); /* Attempt authentication if cookieless clients are allowed to connect or * if we are definitely expecting some realm to be authenticated */ if (Property.getPropertyBoolean(new SystemConfigKey("security.allowUntrackedWebDAVSessions")) || expectingRealm != null) { for (Enumeration e = req.getHeaders(HttpConstants.HDR_AUTHORIZATION); e.hasMoreElements();) { String val = (String) e.nextElement(); authorize(expectingRealm, val); } } return true; } boolean verifyIp() { try { if (SystemDatabaseFactory.getInstance().verifyIPAddress(req.getRemoteAddr())) { return true; } } catch (Exception e) { log.error("Failed to verify IP address. Considering unauthorized.", e); } if (log.isDebugEnabled()) log.debug(req.getRemoteHost() + " is not authorized"); res.setStatus(HttpServletResponse.SC_FORBIDDEN); return false; } /** * Authorise the provided realm using the data the * {@link HttpConstants#HDR_AUTHORIZATION} header. * * @param expectingRealm realm authenticating against * @param authorization authorisation data from * {@link HttpConstants#HDR_AUTHORIZATION} header. * * @throws IOException on any serious error * @throws UserNotFoundException if user cannot be found * @throws DAVAuthenticationRequiredException if authorisation data is wrong */ public void authorize(String expectingRealm, String authorization) throws IOException, UserNotFoundException, DAVAuthenticationRequiredException { int idx = authorization.indexOf(' '); if (idx == -1 || idx == authorization.length() - 1) { throw new DAVAuthenticationRequiredException(expectingRealm); } // Authenticate the user String method = authorization.substring(0, idx); if (!method.equalsIgnoreCase("basic")) { throw new DAVAuthenticationRequiredException(expectingRealm); } // Extract the credentials - should be ticket:tunnel String encoded = authorization.substring(idx + 1); String credentials = new String(Base64.decode(encoded)); idx = credentials.indexOf(':'); if (idx == 0 || idx == -1) { throw new DAVAuthenticationRequiredException(expectingRealm); } // Get the user credentials String username = credentials.substring(0, idx); if (expectingRealm == null) { /* * If we wern't expecting authentication, but we got it anyway, the * client probably doesn't support cookies. */ AuthenticationScheme authScheme = (DefaultAuthenticationScheme) req.getSession() .getAttribute(Constants.AUTH_SESSION); if (authScheme != null) { throw new IOException( "Not expecting a realm, yet an authentication session is available. This is unexpected!"); } doAuth(expectingRealm, username, DAVServlet.configureAuthenticationScheme(req, res)); /* * We now can get the sessionInfo object for this session and make * it temporary this will ensure it is destroyed once the request is * complete. */ sessionInfo = LogonControllerFactory.getInstance().getSessionInfo(req); sessionInfo.setTemporary(true); } else if (expectingRealm.equals(WebDAVAuthenticationModule.DEFAULT_REALM)) { AuthenticationScheme authScheme = (DefaultAuthenticationScheme) req.getSession() .getAttribute(Constants.AUTH_SESSION); if (authScheme == null) { throw new IOException("No authentication scheme initialised."); } doAuth(expectingRealm, username, authScheme); /* * We now can get the sessionInfo object for this session and make * it temporary this will ensure it is destroyed once the request is * complete. */ if (sessionInfo == null) { sessionInfo = LogonControllerFactory.getInstance().getSessionInfo(req); } } else { if (log.isDebugEnabled()) log.debug("Logging " + username + " [" + req.getRemoteHost() + "] onto realm " + expectingRealm + " using Basic authentication for session " + req.getSession().getId()); // subAuths.put(expectingRealm, new AuthPair(username, // password.toCharArray())); } req.getSession().removeAttribute(ATTR_EXPECTING_REALM_AUTHENTICATION); req.getSession().removeAttribute(Constants.AUTH_SENT); /* Logging method */ if (log.isDebugEnabled()) log.debug(req.getMethod() + ' ' + req.getRequestURI() + ' ' + req.getProtocol()); } private void doAuth(String expectingRealm, String username, AuthenticationScheme authScheme) throws DAVAuthenticationRequiredException, IOException { if (authScheme == null) { throw new DAVAuthenticationRequiredException("No valid authentication scheme."); } // Find user try { User user = UserDatabaseManager.getInstance().getDefaultUserDatabase().getAccount(username); authScheme.setUser(user); LogonAction.authenticate(authScheme, req); LogonAction.finishAuthentication(authScheme, req, res); } catch (InvalidLoginCredentialsException ilce) { // Incorrect details, try again throw new DAVAuthenticationRequiredException(expectingRealm); } catch (Exception e) { IOException ioe = new IOException("Failed to authenticate using scheme."); ioe.initCause(e); throw ioe; } } public HttpServletResponse getResponse() { return (HttpServletResponse) res; } /* ====================================================================== */ /* Request methods */ /* ====================================================================== */ /** * <p> * Get the request object. * </p> */ public HttpServletRequest getRequest() { return req; } /** * <p> * Return the path originally requested by the client. * </p> */ public String getMethod() { return this.req.getMethod(); } /** * <p> * Return the path for this transaction. This will be the path as the client * sees it less the first element * * @return path */ public String getPath() { return path; // String path = this.req.getPathInfo(); // if (path == null) return ""; // if ((path.length() > 0) && (path.charAt(0) == '/')) { // return path.substring(1); // } else { // return path; // } } public boolean isRequiredRootRedirect() { return false; } /** * <p> * Return the path originally requested by the client encoded. * </p> */ public String getPathEncoded() { return DAVUtilities.encodePath(getPath()); } /** * <p> * Return the depth requested by the client for this transaction. * </p> */ public int getDepth() { String depth = req.getHeader("Depth"); if (depth == null) return INFINITY; if ("infinity".equals(depth)) return INFINITY; try { return Integer.parseInt(depth); } catch (NumberFormatException exception) { throw new DAVException(412, "Unable to parse depth", exception); } } /** * <p> * Return a {@link URI} */ public URI getDestination() { String destination = this.req.getHeader("Destination"); if (destination != null) try { return this.base.relativize(new URI(destination.replaceAll(" ", "%20"))); } catch (URISyntaxException exception) { throw new DAVException(412, "Can't parse destination", exception); } return null; } /** * <p> * Return the overwrite flag requested by the client for this transaction. * </p> */ public boolean getOverwrite() { String overwrite = req.getHeader("Overwrite"); if (overwrite == null) return true; if ("T".equals(overwrite)) return true; if ("F".equals(overwrite)) return false; throw new DAVException(412, "Unable to parse overwrite flag"); } /** * <p> * Check if the client requested a date-based conditional operation. * </p> */ public Date getIfModifiedSince() { String name = "If-Modified-Since"; if (this.req.getHeader(name) == null) return null; return new Date(this.req.getDateHeader(name)); } /* ====================================================================== */ /* Response methods */ /* ====================================================================== */ /** * <p> * Set the HTTP status code of the response. * </p> */ public void setStatus(int status) { this.res.setStatus(status); } /** * <p> * Set the HTTP <code>Content-Type</code> header. * </p> */ public void setContentType(String type) { this.res.setContentType(type); } /** * <p> * Set an HTTP header in the response. * </p> */ public void setHeader(String name, String value) { this.res.setHeader(name, value); } /** * <p> * Set an HTTP header in the response. * </p> * * @param name name * @param value value */ public void setDateHeader(String name, int value) { this.res.setDateHeader(name, value); } /* ====================================================================== */ /* I/O methods */ /* ====================================================================== */ /** * <p> * Read from the body of the original request. * </p> */ public InputStream getInputStream() throws IOException { /* We don't support ranges */ if (req.getHeader("Content-Range") != null) throw new DAVException(501, "Content-Range not supported"); if (this.req.getContentLength() >= 0) this.req.getInputStream(); String len = this.req.getHeader("Content-Length"); if (len != null) try { if (Long.parseLong(len) > 0) return this.req.getInputStream(); } catch (NumberFormatException exception) { // Unparseable content length header... } // Do not throw an exception, this could be null without an error // condition return null; } /** * <p> * Write the body of the response. * </p> */ public OutputStream getOutputStream() throws IOException { if (SystemProperties.get("adito.webdav.debug", "false").equals("true")) return new TempOutputStream(this.res.getOutputStream()); else return this.res.getOutputStream(); } class TempOutputStream extends OutputStream { OutputStream out; StringBuffer wbBuf; TempOutputStream(OutputStream out) { this.out = out; wbBuf = new StringBuffer(); } public void write(byte[] buf, int off, int len) throws IOException { wbBuf.append(new String(buf, off, len)); out.write(buf, off, len); } public void write(int b) throws IOException { wbBuf.append((byte) b); out.write((byte) b); } public void flush() throws IOException { log.info(wbBuf.toString()); wbBuf.setLength(0); out.flush(); } } /** * <p> * Write the body of the response. * </p> */ public PrintWriter write(String encoding) throws IOException { return new PrintWriter(new OutputStreamWriter(this.getOutputStream(), encoding)); } /* ====================================================================== */ /* Lookup methods */ /* ====================================================================== */ /** * <p> * Look up the final URI of a {@link VFSResource} as visible from the HTTP * client requesting this transaction. * </p> */ public URI lookup(VFSResource resource) { URI uri = resource.getRelativeURI(); URI resolved = null; if (uri == null || uri.toString().equals("")) { resolved = this.base; } else { resolved = this.base.resolve(uri).normalize(); ; } return resolved; } public PasswordCredentials getCredentials() { String authorization = req.getHeader("Authorization"); if (authorization == null) { return null; } int idx = authorization.indexOf(' '); if (idx == -1 || idx == authorization.length() - 1) { return null; } // Authenticate the user String method = authorization.substring(0, idx); if (!method.equalsIgnoreCase("basic")) { return null; } // Extract the credentials - should be ticket:tunnel String encoded = authorization.substring(idx + 1); String credentials = new String(Base64.decode(encoded)); idx = credentials.indexOf(':'); if (idx == 0 || idx == -1) { return null; } // Get the user credentials String username = credentials.substring(0, idx); String password = credentials.substring(idx + 1); return new PasswordCredentials(username, password.toCharArray()); } /** * Get the session info for this transaction. The user and other session * related objects may be found here. * * @return session info */ public SessionInfo getSessionInfo() { return sessionInfo; } /** * Check if the supplied resource path is valid for this transaction path. * This will be used to force a redirect to the required path if not. * * @param fullResourcePath * @return is resource path */ public boolean isResourcePath(String fullResourcePath) { String fullUri = DAVUtilities.stripTrailingSlash(DAVUtilities.stripLeadingSlash(fullResourcePath)); return fullUri.equals(getPath()); } void configureFromRequest() throws URISyntaxException { String scheme = this.req.getScheme(); String host = this.req.getServerName(); String basePath = DAVUtilities.concatenatePaths(this.req.getServletPath(), this.req.getPathInfo()); int port = this.req.getServerPort(); this.base = new URI(scheme, null, host, port, basePath, null, null); this.base = this.base.normalize(); path = DAVUtilities .stripTrailingSlash(DAVUtilities.stripLeadingSlash(DAVUtilities.stripFirstPath(base.getPath()))); } }