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