001/*
002 * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Florent Guillaume
018 */
019package org.nuxeo.ecm.core.io.download;
020
021import java.io.IOException;
022
023import javax.servlet.http.HttpServletRequest;
024
025import org.apache.commons.logging.Log;
026import org.apache.commons.logging.LogFactory;
027import org.nuxeo.common.utils.RFC2231;
028import org.nuxeo.ecm.core.io.download.DownloadService.ByteRange;
029
030/**
031 * Helper class related to the download service.
032 *
033 * @since 7.3
034 */
035public class DownloadHelper {
036
037    private static final Log log = LogFactory.getLog(DownloadHelper.class);
038
039    public static final String INLINE = "inline";
040
041    // tomcat catalina
042    private static final String CLIENT_ABORT_EXCEPTION = "ClientAbortException";
043
044    // jetty (with CamelCase "Eof")
045    private static final String EOF_EXCEPTION = "EofException";
046
047    // utility class
048    private DownloadHelper() {
049    }
050
051    /**
052     * Parses a byte range.
053     *
054     * @param range the byte range as a string
055     * @param length the file length
056     * @return the byte range, or {@code null} if it couldn't be parsed.
057     */
058    public static ByteRange parseRange(String range, long length) {
059        try {
060            // TODO does no support multiple ranges
061            if (!range.startsWith("bytes=") || range.indexOf(',') >= 0) {
062                return null;
063            }
064            int i = range.indexOf('-', 6);
065            if (i < 0) {
066                return null;
067            }
068            String start = range.substring(6, i).trim();
069            String end = range.substring(i + 1).trim();
070            long rangeStart = 0;
071            long rangeEnd = length - 1;
072            if (start.isEmpty()) {
073                if (end.isEmpty()) {
074                    return null;
075                }
076                rangeStart = length - Long.parseLong(end);
077                if (rangeStart < 0) {
078                    rangeStart = 0;
079                }
080            } else {
081                rangeStart = Long.parseLong(start);
082                if (!end.isEmpty()) {
083                    rangeEnd = Long.parseLong(end);
084                }
085            }
086            if (rangeStart > rangeEnd) {
087                return null;
088            }
089            return new ByteRange(rangeStart, rangeEnd);
090        } catch (NumberFormatException e) {
091            return null;
092        }
093    }
094
095    /**
096     * Generates a {@code Content-Disposition} string based on the servlet request for a given filename.
097     * <p>
098     * The value follows RFC2231.
099     *
100     * @param request the http servlet request
101     * @param filename the filename
102     * @return a full string to set as value of a {@code Content-Disposition} header
103     */
104    public static String getRFC2231ContentDisposition(HttpServletRequest request, String filename) {
105        return getRFC2231ContentDisposition(request, filename, null);
106    }
107
108    /**
109     * Generates a {@code Content-Disposition} string for a given filename.
110     * <p>
111     * The value follows RFC2231.
112     *
113     * @param request the http servlet request
114     * @param filename the filename
115     * @param inline how to set the content disposition; {@code TRUE} for {@code inline}, {@code FALSE} for
116     *            {@code attachment}, or {@code null} to detect from {@code inline} request parameter or attribute
117     * @return a full string to set as value of a {@code Content-Disposition} header
118     * @since 7.10
119     */
120    public static String getRFC2231ContentDisposition(HttpServletRequest request, String filename, Boolean inline) {
121        String userAgent = request.getHeader("User-Agent");
122        boolean binline;
123        if (inline == null) {
124            String inlineParam = request.getParameter(INLINE);
125            if (inlineParam == null) {
126                inlineParam = (String) request.getAttribute(INLINE);
127            }
128            binline = inlineParam != null && !"false".equals(inlineParam);
129        } else {
130            binline = inline.booleanValue();
131        }
132        return RFC2231.encodeContentDisposition(filename, binline, userAgent);
133    }
134
135    public static boolean isClientAbortError(Throwable t) {
136        int loops = 20; // no infinite loop
137        while (t != null && loops > 0) {
138            if (t instanceof IOException) {
139                // handle all IOException that are ClientAbortException by looking
140                // at their class name since the package name is not the same for
141                // jboss, glassfish, tomcat and jetty and we don't want to add
142                // implementation specific build dependencies to this project
143                String name = t.getClass().getSimpleName();
144                if (CLIENT_ABORT_EXCEPTION.equals(name) || EOF_EXCEPTION.equals(name)) {
145                    return true;
146                }
147            }
148            loops--;
149            t = t.getCause();
150        }
151        return false;
152    }
153
154    public static void logClientAbort(Throwable t) {
155        log.debug("Client disconnected: " + unwrapException(t).getMessage());
156    }
157
158    private static Throwable unwrapException(Throwable t) {
159        while (t.getCause() != null) {
160            t = t.getCause();
161        }
162        return t;
163    }
164
165    /**
166     * Re-throws the passed exception except if it corresponds to a client disconnect, for which logging doesn't bring
167     * us anything.
168     *
169     * @param e the original exception
170     * @throws IOException if this is not a client disconnect
171     */
172    public static void handleClientDisconnect(IOException e) throws IOException {
173        if (isClientAbortError(e)) {
174            logClientAbort(e);
175        } else {
176            // unexpected problem, let traditional error management handle it
177            throw e; // NOSONAR
178        }
179    }
180
181}