package org.dcache.webdav; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import io.milton.http.AbstractWrappingResponseHandler; import io.milton.http.AuthenticationService; import io.milton.http.Range; import io.milton.http.Request; import io.milton.http.Response; import io.milton.http.exceptions.BadRequestException; import io.milton.http.exceptions.NotAuthorizedException; import io.milton.http.exceptions.NotFoundException; import io.milton.http.values.ValueAndType; import io.milton.http.webdav.PropFindResponse; import io.milton.http.webdav.PropFindResponse.NameAndError; import io.milton.resource.GetableResource; import io.milton.resource.Resource; import io.milton.servlet.ServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Required; import org.stringtemplate.v4.ST; import javax.security.auth.Subject; import javax.xml.namespace.QName; import java.io.IOException; import java.io.OutputStream; import java.net.URI; import java.security.AccessController; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import diskCacheV111.util.FsPath; import org.dcache.auth.attributes.HomeDirectory; import org.dcache.auth.attributes.LoginAttribute; import org.dcache.auth.attributes.RootDirectory; import org.dcache.util.Slf4jSTErrorListener; import static com.google.common.base.Preconditions.checkNotNull; import static io.milton.http.Response.Status.*; import static org.dcache.webdav.AuthenticationHandler.DCACHE_LOGIN_ATTRIBUTES; /** * This class controls how Milton responds under different circumstances by * decorating the standard response handler. This is done to provide template- * based custom error pages, to add support for additional headers in the * response, and to work-around some bugs. */ public class DcacheResponseHandler extends AbstractWrappingResponseHandler { private static final Logger log = LoggerFactory.getLogger(DcacheResponseHandler.class); public static final String HTML_TEMPLATE_NAME = "errorpage"; private static final Splitter PATH_SPLITTER = Splitter.on('/').omitEmptyStrings(); private final ImmutableMap<Response.Status,String> ERRORS = ImmutableMap.<Response.Status,String>builder() .put(SC_INTERNAL_SERVER_ERROR, "INTERNAL SERVER ERROR") .put(SC_FORBIDDEN, "PERMISSION DENIED") .put(SC_BAD_REQUEST, "BAD REQUEST") .put(SC_NOT_IMPLEMENTED, "NOT IMPLEMENTED") .put(SC_CONFLICT, "CONFLICT") .put(SC_UNAUTHORIZED, "UNAUTHORIZED") .put(SC_METHOD_NOT_ALLOWED, "METHOD NOT ALLOWED") .put(SC_NOT_FOUND, "FILE NOT FOUND") .build(); private AuthenticationService _authenticationService; private String _staticContentPath; private ReloadableTemplate _template; private ImmutableMap<String, String> _templateConfig; private PathMapper pathMapper; public void setPathMapper(PathMapper mapper) { pathMapper = checkNotNull(mapper); } public void setAuthenticationService(AuthenticationService authenticationService) { _authenticationService = authenticationService; } /** * Sets the resource containing the StringTemplateGroup for * directory listing. */ public void setReloadableTemplate(ReloadableTemplate template) { _template = template; } @Required public void setTemplateConfig(ImmutableMap<String, String> config) { _templateConfig = config; } /** * Returns the static content path. */ public String getStaticContentPath() { return _staticContentPath; } /** * The static content path is the path under which the service * exports the static content. This typically contains stylesheets * and image files. */ public void setStaticContentPath(String path) { _staticContentPath = path; } @Override public void respondNotFound(Response response, Request request) { errorResponse(request, response, SC_NOT_FOUND); } @Override public void respondUnauthorised(Resource resource, Response response, Request request) { // If GET on the root results in an authorization failure, we redirect to the users // home directory for convenience. if (request.getAbsolutePath().equals("/") && request.getMethod() == Request.Method.GET) { Set<LoginAttribute> login = (Set<LoginAttribute>) ServletRequest.getRequest().getAttribute(DCACHE_LOGIN_ATTRIBUTES); FsPath userRoot = FsPath.ROOT; String userHome = "/"; for (LoginAttribute attribute : login) { if (attribute instanceof RootDirectory) { userRoot = FsPath.create(((RootDirectory) attribute).getRoot()); } else if (attribute instanceof HomeDirectory) { userHome = ((HomeDirectory) attribute).getHome(); } } try { FsPath redirectFullPath = userRoot.chroot(userHome); String redirectPath = pathMapper.asRequestPath(redirectFullPath); if (!redirectPath.equals("/")) { respondRedirect(response, request, redirectPath); } return; } catch (IllegalArgumentException ignored) { } } List<String> challenges = _authenticationService.getChallenges(resource, request); response.setAuthenticateHeader(challenges); errorResponse(request, response, SC_UNAUTHORIZED); } @Override public void respondMethodNotImplemented(Resource resource, Response response, Request request) { errorResponse(request, response, SC_NOT_IMPLEMENTED); } @Override public void respondMethodNotAllowed(Resource resource, Response response, Request request) { errorResponse(request, response, SC_METHOD_NOT_ALLOWED); } @Override public void respondServerError(Request request, Response response, String reason) { errorResponse(request, response, SC_INTERNAL_SERVER_ERROR); } @Override public void respondConflict(Resource resource, Response response, Request request, String reason) { errorResponse(request, response, SC_CONFLICT); } @Override public void respondForbidden(Resource resource, Response response, Request request) { errorResponse(request, response, SC_FORBIDDEN); } private void errorResponse(Request request, Response response, Response.Status status) { try { String decodedPath = URI.create(request.getAbsoluteUrl()).getPath(); String error = generateErrorPage(decodedPath, status); response.setStatus(status); response.setContentTypeHeader("text/html"); OutputStream out = response.getOutputStream(); out.write(error.getBytes()); } catch (IOException ex) { log.warn("exception writing content"); } } /** * Generates an error page. */ private String generateErrorPage(String path, Response.Status status) { String[] base = Iterables.toArray(PATH_SPLITTER.split(path), String.class); ST template = _template.getInstanceOf(HTML_TEMPLATE_NAME); if (template == null) { return templateNotFoundErrorPage(_template.getPath(), HTML_TEMPLATE_NAME); } template.add("path", UrlPathWrapper.forPaths(base)); template.add("base", UrlPathWrapper.forEmptyPath()); template.add("static", _staticContentPath); template.add("errorcode", status.toString()); template.add("errormessage", ERRORS.get(status)); template.add("config", _templateConfig); Subject subject = Subject.getSubject(AccessController.getContext()); if (subject != null) { template.add("subject", subject.getPrincipals().toString()); } return template.render(); } public static String templateNotFoundErrorPage(String filename, String template) { return "<html><head><title>Broken dCache installation</title></head>" + "<body><div style='margin: 5px; border: 2px solid red; padding: 2px 10px;'>" + "<h1>Broken dCache installation</h1>" + "<p style='width: 50em'>The webdav service of your dCache " + "installation cannot generate this page correctly because it could " + "not find the <tt style='font-size: 120%; color: green;'>" + template + "</tt> template. Please check the file <tt>" + filename + "</tt> for a line that starts:</p>" + "<code>" + template + "(...) ::= <<</code>" + "<p style='width: 50em'>For more details on the format of this file, see the " + "<a href='https://theantlrguy.atlassian.net/wiki/display/ST4/Group+file+syntax'>" + "template language documentation</a>.</p></div></body></html>"; } @Override public void respondPropFind(List<PropFindResponse> propFindResponses, Response response, Request request, Resource r) { /* Milton adds properties with a null value to the PROPFIND response. * gvfs doesn't like this and it is unclear whether or not this violates * RFC 2518. * * To work around this issue we move such properties to the set of * unknown properties. * * See http://lists.justthe.net/pipermail/milton-users/2012-June/001363.html */ for (PropFindResponse propFindResponse: propFindResponses) { Map<Response.Status,List<PropFindResponse.NameAndError>> errors = propFindResponse.getErrorProperties(); List<NameAndError> unknownProperties = errors.get(Response.Status.SC_NOT_FOUND); if (unknownProperties == null) { unknownProperties = Lists.newArrayList(); errors.put(Response.Status.SC_NOT_FOUND, unknownProperties); } Iterator<Map.Entry<QName, ValueAndType>> iterator = propFindResponse.getKnownProperties().entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<QName, ValueAndType> entry = iterator.next(); if (entry.getValue().getValue() == null) { unknownProperties.add(new NameAndError(entry.getKey(), null)); iterator.remove(); } } } super.respondPropFind(propFindResponses, response, request, r); } @Override public void respondHead(Resource resource, Response response, Request request) { super.respondHead(resource, response, request); rfc3230(resource, response); } @Override public void respondPartialContent(GetableResource resource, Response response, Request request, Map<String, String> params, List<Range> ranges) throws NotAuthorizedException, BadRequestException, NotFoundException { super.respondPartialContent(resource, response, request, params, ranges); rfc3230(resource, response); } @Override public void respondPartialContent(GetableResource resource, Response response, Request request, Map<String,String> params, Range range) throws NotAuthorizedException, BadRequestException, NotFoundException { Long contentLength = resource.getContentLength(); /* [RFC 2616, section 14.35.1] * * "If the last-byte-pos value is absent, or if the value is greater than or equal to the * current length of the entity-body, last-byte-pos is taken to be equal to one less than * the current length of the entity- body in bytes." * * Milton ought to do this, but it doesn't. */ if (contentLength != null && range.getFinish() != null && range.getFinish() >= contentLength) { range = new Range(range.getStart(), contentLength - 1); } super.respondPartialContent(resource, response, request, params, range); rfc3230(resource, response); } @Override public void respondContent(Resource resource, Response response, Request request, Map<String, String> params) throws NotAuthorizedException, BadRequestException, NotFoundException { super.respondContent(resource, response, request, params); rfc3230(resource, response); } private void rfc3230(Resource resource, Response response) { if(resource instanceof DcacheFileResource) { DcacheFileResource file = (DcacheFileResource) resource; String digest = file.getRfc3230Digest(); if(!digest.isEmpty()) { response.setNonStandardHeader("Digest", digest); } } } }