/** * Copyright (c) 2000-present Liferay, Inc. All rights reserved. * * This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 2.1 of the License, or (at your option) * any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. */ package com.liferay.portal.kernel.servlet; import com.liferay.portal.kernel.log.Log; import com.liferay.portal.kernel.log.LogFactoryUtil; import com.liferay.portal.kernel.nio.charset.CharsetEncoderUtil; import com.liferay.portal.kernel.util.ArrayUtil; import com.liferay.portal.kernel.util.FileUtil; import com.liferay.portal.kernel.util.GetterUtil; import com.liferay.portal.kernel.util.MimeTypesUtil; import com.liferay.portal.kernel.util.PropsKeys; import com.liferay.portal.kernel.util.PropsUtil; import com.liferay.portal.kernel.util.RandomAccessInputStream; import com.liferay.portal.kernel.util.ServerDetector; import com.liferay.portal.kernel.util.StreamUtil; import com.liferay.portal.kernel.util.StringBundler; import com.liferay.portal.kernel.util.StringPool; import com.liferay.portal.kernel.util.StringUtil; import com.liferay.portal.kernel.util.URLCodec; import com.liferay.portal.kernel.util.Validator; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.SocketException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @author Brian Wing Shun Chan * @author Shuyang Zhou */ public class ServletResponseUtil { public static List<Range> getRanges( HttpServletRequest request, HttpServletResponse response, long length) throws IOException { String rangeString = request.getHeader(HttpHeaders.RANGE); if (Validator.isNull(rangeString)) { return Collections.emptyList(); } if (!rangeString.matches(_RANGE_REGEX)) { throw new IOException( "Range header does not match regular expression " + rangeString); } List<Range> ranges = new ArrayList<>(); String[] rangeFields = StringUtil.split(rangeString.substring(6)); if (rangeFields.length > _MAX_RANGE_FIELDS) { StringBundler sb = new StringBundler(8); sb.append("Request range "); sb.append(rangeString); sb.append(" with "); sb.append(rangeFields.length); sb.append(" range fields has exceeded maximum allowance as "); sb.append("specified by the property \""); sb.append(PropsKeys.WEB_SERVER_SERVLET_MAX_RANGE_FIELDS); sb.append("\""); throw new IOException(sb.toString()); } for (String rangeField : rangeFields) { int index = rangeField.indexOf(StringPool.DASH); long start = GetterUtil.getLong(rangeField.substring(0, index), -1); long end = GetterUtil.getLong( rangeField.substring(index + 1, rangeField.length()), -1); if (start == -1) { start = length - end; end = length - 1; } else if ((end == -1) || (end > (length - 1))) { end = length - 1; } if (start > end) { throw new IOException( "Range start " + start + " is greater than end " + end); } Range range = new Range(start, end, length); ranges.add(range); } return ranges; } public static boolean isClientAbortException(IOException ioe) { Class<?> clazz = ioe.getClass(); String className = clazz.getName(); if (className.equals(_CLIENT_ABORT_EXCEPTION)) { return true; } else { return false; } } public static void sendFile( HttpServletRequest request, HttpServletResponse response, String fileName, byte[] bytes) throws IOException { sendFile(request, response, fileName, bytes, null); } public static void sendFile( HttpServletRequest request, HttpServletResponse response, String fileName, byte[] bytes, String contentType) throws IOException { sendFile(request, response, fileName, bytes, contentType, null); } public static void sendFile( HttpServletRequest request, HttpServletResponse response, String fileName, byte[] bytes, String contentType, String contentDispositionType) throws IOException { setHeaders( request, response, fileName, contentType, contentDispositionType); write(response, bytes); } public static void sendFile( HttpServletRequest request, HttpServletResponse response, String fileName, InputStream inputStream) throws IOException { sendFile(request, response, fileName, inputStream, null); } public static void sendFile( HttpServletRequest request, HttpServletResponse response, String fileName, InputStream inputStream, long contentLength, String contentType) throws IOException { sendFile( request, response, fileName, inputStream, contentLength, contentType, null); } public static void sendFile( HttpServletRequest request, HttpServletResponse response, String fileName, InputStream inputStream, long contentLength, String contentType, String contentDispositionType) throws IOException { setHeaders( request, response, fileName, contentType, contentDispositionType); write(response, inputStream, contentLength); } public static void sendFile( HttpServletRequest request, HttpServletResponse response, String fileName, InputStream inputStream, String contentType) throws IOException { sendFile(request, response, fileName, inputStream, 0, contentType); } public static void sendFileWithRangeHeader( HttpServletRequest request, HttpServletResponse response, String fileName, InputStream inputStream, long contentLength, String contentType) throws IOException { if (_log.isDebugEnabled()) { _log.debug("Accepting ranges for the file " + fileName); } response.setHeader( HttpHeaders.ACCEPT_RANGES, HttpHeaders.ACCEPT_RANGES_BYTES_VALUE); List<Range> ranges = null; try { ranges = getRanges(request, response, contentLength); } catch (IOException ioe) { _log.error(ioe); response.setHeader( HttpHeaders.CONTENT_RANGE, "bytes */" + contentLength); response.sendError( HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); return; } if ((ranges == null) || ranges.isEmpty()) { sendFile( request, response, fileName, inputStream, contentLength, contentType); } else { if (_log.isDebugEnabled()) { _log.debug( "Request has range header " + request.getHeader(HttpHeaders.RANGE)); } write( request, response, fileName, ranges, inputStream, contentLength, contentType); } } public static void write( HttpServletRequest request, HttpServletResponse response, String fileName, List<Range> ranges, InputStream inputStream, long fullLength, String contentType) throws IOException { OutputStream outputStream = null; try { outputStream = response.getOutputStream(); Range fullRange = new Range(0, fullLength - 1, fullLength); Range firstRange = null; if (!ranges.isEmpty()) { firstRange = ranges.get(0); } if ((firstRange == null) || firstRange.equals(fullRange)) { if (_log.isDebugEnabled()) { _log.debug("Writing full range"); } response.setContentType(contentType); setHeaders( request, response, fileName, contentType, null, fullRange); copyRange( inputStream, outputStream, fullRange.getStart(), fullRange.getLength()); } else if (ranges.size() == 1) { if (_log.isDebugEnabled()) { _log.debug("Attempting to write a single range"); } Range range = ranges.get(0); response.setContentType(contentType); setHeaders( request, response, fileName, contentType, null, range); response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); copyRange( inputStream, outputStream, range.getStart(), range.getLength()); } else if (ranges.size() > 1) { if (_log.isDebugEnabled()) { _log.debug("Attempting to write multiple ranges"); } ServletOutputStream servletOutputStream = (ServletOutputStream)outputStream; String boundary = "liferay-multipart-boundary-" + System.currentTimeMillis(); String multipartContentType = "multipart/byteranges; boundary=" + boundary; response.setContentType(multipartContentType); setHeaders( request, response, fileName, multipartContentType, null); response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); for (int i = 0; i < ranges.size(); i++) { Range range = ranges.get(i); servletOutputStream.println(); servletOutputStream.println( StringPool.DOUBLE_DASH + boundary); servletOutputStream.println( HttpHeaders.CONTENT_TYPE + ": " + contentType); servletOutputStream.println( HttpHeaders.CONTENT_RANGE + ": " + range.getContentRange()); servletOutputStream.println(); inputStream = copyRange( inputStream, outputStream, range.getStart(), range.getLength()); } servletOutputStream.println(); servletOutputStream.println( StringPool.DOUBLE_DASH + boundary + StringPool.DOUBLE_DASH); } } finally { try { inputStream.close(); } catch (IOException ioe) { } } } public static void write( HttpServletResponse response, BufferCacheServletResponse bufferCacheServletResponse) throws IOException { if (bufferCacheServletResponse.isByteMode()) { write(response, bufferCacheServletResponse.getByteBuffer()); } else if (bufferCacheServletResponse.isCharMode()) { write(response, bufferCacheServletResponse.getCharBuffer()); } } public static void write(HttpServletResponse response, byte[] bytes) throws IOException { write(response, bytes, 0, 0); } public static void write( HttpServletResponse response, byte[] bytes, int offset, int contentLength) throws IOException { try { // LEP-3122 if (!response.isCommitted()) { // LEP-536 if (contentLength == 0) { contentLength = bytes.length; } response.setContentLength(contentLength); response.flushBuffer(); if (response instanceof BufferCacheServletResponse) { BufferCacheServletResponse bufferCacheServletResponse = (BufferCacheServletResponse)response; bufferCacheServletResponse.setByteBuffer( ByteBuffer.wrap(bytes, offset, contentLength)); } else { ServletOutputStream servletOutputStream = response.getOutputStream(); if ((contentLength == 0) && ServerDetector.isJetty()) { } else { servletOutputStream.write(bytes, offset, contentLength); } } } } catch (IOException ioe) { if ((ioe instanceof SocketException) || isClientAbortException(ioe)) { if (_log.isWarnEnabled()) { _log.warn(ioe); } } else { throw ioe; } } } public static void write(HttpServletResponse response, byte[][] bytesArray) throws IOException { try { // LEP-3122 if (!response.isCommitted()) { long contentLength = 0; for (byte[] bytes : bytesArray) { contentLength += bytes.length; } setContentLength(response, contentLength); response.flushBuffer(); ServletOutputStream servletOutputStream = response.getOutputStream(); for (byte[] bytes : bytesArray) { servletOutputStream.write(bytes); } } } catch (IOException ioe) { if ((ioe instanceof SocketException) || isClientAbortException(ioe)) { if (_log.isWarnEnabled()) { _log.warn(ioe); } } else { throw ioe; } } } public static void write( HttpServletResponse response, ByteBuffer byteBuffer) throws IOException { if (response instanceof BufferCacheServletResponse) { BufferCacheServletResponse bufferCacheServletResponse = (BufferCacheServletResponse)response; bufferCacheServletResponse.setByteBuffer(byteBuffer); } else { write( response, byteBuffer.array(), byteBuffer.arrayOffset() + byteBuffer.position(), byteBuffer.arrayOffset() + byteBuffer.limit()); } } public static void write( HttpServletResponse response, CharBuffer charBuffer) throws IOException { if (response instanceof BufferCacheServletResponse) { BufferCacheServletResponse bufferCacheServletResponse = (BufferCacheServletResponse)response; bufferCacheServletResponse.setCharBuffer(charBuffer); } else { ByteBuffer byteBuffer = CharsetEncoderUtil.encode( StringPool.UTF8, charBuffer); write(response, byteBuffer); } } public static void write(HttpServletResponse response, File file) throws IOException { if (response instanceof BufferCacheServletResponse) { BufferCacheServletResponse bufferCacheServletResponse = (BufferCacheServletResponse)response; ByteBuffer byteBuffer = ByteBuffer.wrap(FileUtil.getBytes(file)); bufferCacheServletResponse.setByteBuffer(byteBuffer); } else { FileInputStream fileInputStream = new FileInputStream(file); try (FileChannel fileChannel = fileInputStream.getChannel()) { long contentLength = fileChannel.size(); setContentLength(response, contentLength); response.flushBuffer(); fileChannel.transferTo( 0, contentLength, Channels.newChannel(response.getOutputStream())); } } } public static void write( HttpServletResponse response, InputStream inputStream) throws IOException { write(response, inputStream, 0); } public static void write( HttpServletResponse response, InputStream inputStream, long contentLength) throws IOException { if (response.isCommitted()) { StreamUtil.cleanUp(inputStream); return; } if (contentLength > 0) { response.setHeader( HttpHeaders.CONTENT_LENGTH, String.valueOf(contentLength)); } response.flushBuffer(); StreamUtil.transfer(inputStream, response.getOutputStream()); } public static void write(HttpServletResponse response, String s) throws IOException { if (response instanceof BufferCacheServletResponse) { BufferCacheServletResponse bufferCacheServletResponse = (BufferCacheServletResponse)response; bufferCacheServletResponse.setString(s); } else { ByteBuffer byteBuffer = CharsetEncoderUtil.encode( StringPool.UTF8, s); write(response, byteBuffer); } } protected static InputStream copyRange( InputStream inputStream, OutputStream outputStream, long start, long length) throws IOException { if (inputStream instanceof FileInputStream) { FileInputStream fileInputStream = (FileInputStream)inputStream; FileChannel fileChannel = fileInputStream.getChannel(); fileChannel.transferTo( start, length, Channels.newChannel(outputStream)); return fileInputStream; } else if (inputStream instanceof ByteArrayInputStream) { ByteArrayInputStream byteArrayInputStream = (ByteArrayInputStream)inputStream; byteArrayInputStream.reset(); byteArrayInputStream.skip(start); StreamUtil.transfer(byteArrayInputStream, outputStream, length); return byteArrayInputStream; } else if (inputStream instanceof RandomAccessInputStream) { RandomAccessInputStream randomAccessInputStream = (RandomAccessInputStream)inputStream; randomAccessInputStream.seek(start); StreamUtil.transfer( randomAccessInputStream, outputStream, StreamUtil.BUFFER_SIZE, false, length); return randomAccessInputStream; } return copyRange( new RandomAccessInputStream(inputStream), outputStream, start, length); } protected static void setContentLength( HttpServletResponse response, long contentLength) { response.setHeader( HttpHeaders.CONTENT_LENGTH, String.valueOf(contentLength)); } protected static void setHeaders( HttpServletRequest request, HttpServletResponse response, String fileName, String contentType, String contentDispositionType) { if (_log.isDebugEnabled()) { _log.debug("Sending file of type " + contentType); } // LEP-2201 if (Validator.isNotNull(contentType)) { response.setContentType(contentType); } if (!response.containsHeader(HttpHeaders.CACHE_CONTROL)) { response.setHeader( HttpHeaders.CACHE_CONTROL, HttpHeaders.CACHE_CONTROL_PRIVATE_VALUE); } if (Validator.isNull(fileName)) { return; } String contentDispositionFileName = "filename=\"" + fileName + "\""; // If necessary for non-ASCII characters, encode based on RFC 2184. // However, not all browsers support RFC 2184. See LEP-3127. boolean ascii = true; for (int i = 0; i < fileName.length(); i++) { if (!Validator.isAscii(fileName.charAt(i))) { ascii = false; break; } } if (!ascii) { String encodedFileName = URLCodec.encodeURL(fileName, true); if (BrowserSnifferUtil.isIe(request)) { contentDispositionFileName = "filename=\"" + encodedFileName + "\""; } else { contentDispositionFileName = "filename*=UTF-8''" + encodedFileName; } } if (Validator.isNull(contentDispositionType)) { String extension = GetterUtil.getString( FileUtil.getExtension(fileName)); extension = StringUtil.toLowerCase(extension); String[] mimeTypesContentDispositionInline = null; try { mimeTypesContentDispositionInline = PropsUtil.getArray( PropsKeys.MIME_TYPES_CONTENT_DISPOSITION_INLINE); } catch (Exception e) { mimeTypesContentDispositionInline = new String[0]; } if (ArrayUtil.contains( mimeTypesContentDispositionInline, extension)) { contentDispositionType = HttpHeaders.CONTENT_DISPOSITION_INLINE; contentType = MimeTypesUtil.getContentType(fileName); response.setContentType(contentType); } else { contentDispositionType = HttpHeaders.CONTENT_DISPOSITION_ATTACHMENT; } } StringBundler sb = new StringBundler(4); sb.append(contentDispositionType); sb.append(StringPool.SEMICOLON); sb.append(StringPool.SPACE); sb.append(contentDispositionFileName); if (_log.isDebugEnabled()) { _log.debug("Setting content disposition header " + sb.toString()); } response.setHeader(HttpHeaders.CONTENT_DISPOSITION, sb.toString()); } protected static void setHeaders( HttpServletRequest request, HttpServletResponse response, String fileName, String contentType, String contentDispositionType, Range range) { setHeaders( request, response, fileName, contentType, contentDispositionType); if (range != null) { response.setHeader( HttpHeaders.CONTENT_RANGE, range.getContentRange()); response.setHeader( HttpHeaders.CONTENT_LENGTH, String.valueOf(range.getLength())); } } private static final String _CLIENT_ABORT_EXCEPTION = "org.apache.catalina.connector.ClientAbortException"; private static final int _MAX_RANGE_FIELDS = GetterUtil.getInteger( PropsUtil.get(PropsKeys.WEB_SERVER_SERVLET_MAX_RANGE_FIELDS)); private static final String _RANGE_REGEX = "^bytes=\\d*-\\d*(,\\s?\\d*-\\d*)*$"; private static final Log _log = LogFactoryUtil.getLog( ServletResponseUtil.class); }