001    /**
002     * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
003     *
004     * This library is free software; you can redistribute it and/or modify it under
005     * the terms of the GNU Lesser General Public License as published by the Free
006     * Software Foundation; either version 2.1 of the License, or (at your option)
007     * any later version.
008     *
009     * This library is distributed in the hope that it will be useful, but WITHOUT
010     * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
011     * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
012     * details.
013     */
014    
015    package com.liferay.portal.kernel.servlet;
016    
017    import com.liferay.portal.kernel.log.Log;
018    import com.liferay.portal.kernel.log.LogFactoryUtil;
019    import com.liferay.portal.kernel.nio.charset.CharsetEncoderUtil;
020    import com.liferay.portal.kernel.util.ArrayUtil;
021    import com.liferay.portal.kernel.util.ContentTypes;
022    import com.liferay.portal.kernel.util.FileUtil;
023    import com.liferay.portal.kernel.util.GetterUtil;
024    import com.liferay.portal.kernel.util.HttpUtil;
025    import com.liferay.portal.kernel.util.MimeTypesUtil;
026    import com.liferay.portal.kernel.util.PropsKeys;
027    import com.liferay.portal.kernel.util.PropsUtil;
028    import com.liferay.portal.kernel.util.RandomAccessInputStream;
029    import com.liferay.portal.kernel.util.ServerDetector;
030    import com.liferay.portal.kernel.util.StreamUtil;
031    import com.liferay.portal.kernel.util.StringBundler;
032    import com.liferay.portal.kernel.util.StringPool;
033    import com.liferay.portal.kernel.util.StringUtil;
034    import com.liferay.portal.kernel.util.Validator;
035    
036    import java.io.ByteArrayInputStream;
037    import java.io.File;
038    import java.io.FileInputStream;
039    import java.io.IOException;
040    import java.io.InputStream;
041    import java.io.OutputStream;
042    
043    import java.net.SocketException;
044    
045    import java.nio.ByteBuffer;
046    import java.nio.CharBuffer;
047    import java.nio.channels.Channels;
048    import java.nio.channels.FileChannel;
049    
050    import java.util.ArrayList;
051    import java.util.Collections;
052    import java.util.List;
053    
054    import javax.servlet.ServletOutputStream;
055    import javax.servlet.http.HttpServletRequest;
056    import javax.servlet.http.HttpServletResponse;
057    
058    /**
059     * @author Brian Wing Shun Chan
060     * @author Shuyang Zhou
061     */
062    public class ServletResponseUtil {
063    
064            public static List<Range> getRanges(
065                            HttpServletRequest request, HttpServletResponse response,
066                            long length)
067                    throws IOException {
068    
069                    String rangeString = request.getHeader(HttpHeaders.RANGE);
070    
071                    if (Validator.isNull(rangeString)) {
072                            return Collections.emptyList();
073                    }
074    
075                    if (!rangeString.matches(_RANGE_REGEX)) {
076                            throw new IOException(
077                                    "Range header does not match regular expression " +
078                                            rangeString);
079                    }
080    
081                    List<Range> ranges = new ArrayList<>();
082    
083                    String[] rangeFields = StringUtil.split(rangeString.substring(6));
084    
085                    if (rangeFields.length > _MAX_RANGE_FIELDS) {
086                            StringBundler sb = new StringBundler(8);
087    
088                            sb.append("Request range ");
089                            sb.append(rangeString);
090                            sb.append(" with ");
091                            sb.append(rangeFields.length);
092                            sb.append(" range fields has exceeded maximum allowance as ");
093                            sb.append("specified by the property \"");
094                            sb.append(PropsKeys.WEB_SERVER_SERVLET_MAX_RANGE_FIELDS);
095                            sb.append("\"");
096    
097                            throw new IOException(sb.toString());
098                    }
099    
100                    for (String rangeField : rangeFields) {
101                            int index = rangeField.indexOf(StringPool.DASH);
102    
103                            long start = GetterUtil.getLong(rangeField.substring(0, index), -1);
104                            long end = GetterUtil.getLong(
105                                    rangeField.substring(index + 1, rangeField.length()), -1);
106    
107                            if (start == -1) {
108                                    start = length - end;
109                                    end = length - 1;
110                            }
111                            else if ((end == -1) || (end > (length - 1))) {
112                                    end = length - 1;
113                            }
114    
115                            if (start > end) {
116                                    throw new IOException(
117                                            "Range start " + start + " is greater than end " + end);
118                            }
119    
120                            Range range = new Range(start, end, length);
121    
122                            ranges.add(range);
123                    }
124    
125                    return ranges;
126            }
127    
128            public static boolean isClientAbortException(IOException ioe) {
129                    Class<?> clazz = ioe.getClass();
130    
131                    String className = clazz.getName();
132    
133                    if (className.equals(_CLIENT_ABORT_EXCEPTION)) {
134                            return true;
135                    }
136                    else {
137                            return false;
138                    }
139            }
140    
141            public static void sendFile(
142                            HttpServletRequest request, HttpServletResponse response,
143                            String fileName, byte[] bytes)
144                    throws IOException {
145    
146                    sendFile(request, response, fileName, bytes, null);
147            }
148    
149            public static void sendFile(
150                            HttpServletRequest request, HttpServletResponse response,
151                            String fileName, byte[] bytes, String contentType)
152                    throws IOException {
153    
154                    sendFile(request, response, fileName, bytes, contentType, null);
155            }
156    
157            public static void sendFile(
158                            HttpServletRequest request, HttpServletResponse response,
159                            String fileName, byte[] bytes, String contentType,
160                            String contentDispositionType)
161                    throws IOException {
162    
163                    setHeaders(
164                            request, response, fileName, contentType, contentDispositionType);
165    
166                    write(response, bytes);
167            }
168    
169            public static void sendFile(
170                            HttpServletRequest request, HttpServletResponse response,
171                            String fileName, InputStream inputStream)
172                    throws IOException {
173    
174                    sendFile(request, response, fileName, inputStream, null);
175            }
176    
177            public static void sendFile(
178                            HttpServletRequest request, HttpServletResponse response,
179                            String fileName, InputStream inputStream, long contentLength,
180                            String contentType)
181                    throws IOException {
182    
183                    sendFile(
184                            request, response, fileName, inputStream, contentLength,
185                            contentType, null);
186            }
187    
188            public static void sendFile(
189                            HttpServletRequest request, HttpServletResponse response,
190                            String fileName, InputStream inputStream, long contentLength,
191                            String contentType, String contentDispositionType)
192                    throws IOException {
193    
194                    setHeaders(
195                            request, response, fileName, contentType, contentDispositionType);
196    
197                    write(response, inputStream, contentLength);
198            }
199    
200            public static void sendFile(
201                            HttpServletRequest request, HttpServletResponse response,
202                            String fileName, InputStream inputStream, String contentType)
203                    throws IOException {
204    
205                    sendFile(request, response, fileName, inputStream, 0, contentType);
206            }
207    
208            public static void sendFileWithRangeHeader(
209                            HttpServletRequest request, HttpServletResponse response,
210                            String fileName, InputStream inputStream, long contentLength,
211                            String contentType)
212                    throws IOException {
213    
214                    if (_log.isDebugEnabled()) {
215                            _log.debug("Accepting ranges for the file " + fileName);
216                    }
217    
218                    response.setHeader(
219                            HttpHeaders.ACCEPT_RANGES, HttpHeaders.ACCEPT_RANGES_BYTES_VALUE);
220    
221                    List<Range> ranges = null;
222    
223                    try {
224                            ranges = getRanges(request, response, contentLength);
225                    }
226                    catch (IOException ioe) {
227                            _log.error(ioe);
228    
229                            response.setHeader(
230                                    HttpHeaders.CONTENT_RANGE, "bytes */" + contentLength);
231    
232                            response.sendError(
233                                    HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
234    
235                            return;
236                    }
237    
238                    if ((ranges == null) || ranges.isEmpty()) {
239                            sendFile(
240                                    request, response, fileName, inputStream, contentLength,
241                                    contentType);
242                    }
243                    else {
244                            if (_log.isDebugEnabled()) {
245                                    _log.debug(
246                                            "Request has range header " +
247                                                    request.getHeader(HttpHeaders.RANGE));
248                            }
249    
250                            write(
251                                    request, response, fileName, ranges, inputStream, contentLength,
252                                    contentType);
253                    }
254            }
255    
256            public static void write(
257                            HttpServletRequest request, HttpServletResponse response,
258                            String fileName, List<Range> ranges, InputStream inputStream,
259                            long fullLength, String contentType)
260                    throws IOException {
261    
262                    OutputStream outputStream = null;
263    
264                    try {
265                            outputStream = response.getOutputStream();
266    
267                            Range fullRange = new Range(0, fullLength - 1, fullLength);
268    
269                            Range firstRange = null;
270    
271                            if (!ranges.isEmpty()) {
272                                    firstRange = ranges.get(0);
273                            }
274    
275                            if ((firstRange == null) || firstRange.equals(fullRange)) {
276                                    if (_log.isDebugEnabled()) {
277                                            _log.debug("Writing full range");
278                                    }
279    
280                                    response.setContentType(contentType);
281    
282                                    setHeaders(
283                                            request, response, fileName, contentType, null, fullRange);
284    
285                                    copyRange(
286                                            inputStream, outputStream, fullRange.getStart(),
287                                            fullRange.getLength());
288                            }
289                            else if (ranges.size() == 1) {
290                                    if (_log.isDebugEnabled()) {
291                                            _log.debug("Attempting to write a single range");
292                                    }
293    
294                                    Range range = ranges.get(0);
295    
296                                    response.setContentType(contentType);
297    
298                                    setHeaders(
299                                            request, response, fileName, contentType, null, range);
300    
301                                    response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
302    
303                                    copyRange(
304                                            inputStream, outputStream, range.getStart(),
305                                            range.getLength());
306                            }
307                            else if (ranges.size() > 1) {
308                                    if (_log.isDebugEnabled()) {
309                                            _log.debug("Attempting to write multiple ranges");
310                                    }
311    
312                                    ServletOutputStream servletOutputStream =
313                                            (ServletOutputStream)outputStream;
314    
315                                    String boundary =
316                                            "liferay-multipart-boundary-" + System.currentTimeMillis();
317    
318                                    String multipartContentType =
319                                            "multipart/byteranges; boundary=" + boundary;
320    
321                                    response.setContentType(multipartContentType);
322    
323                                    setHeaders(
324                                            request, response, fileName, multipartContentType, null);
325    
326                                    response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
327    
328                                    for (int i = 0; i < ranges.size(); i++) {
329                                            Range range = ranges.get(i);
330    
331                                            servletOutputStream.println();
332                                            servletOutputStream.println(
333                                                    StringPool.DOUBLE_DASH + boundary);
334                                            servletOutputStream.println(
335                                                    HttpHeaders.CONTENT_TYPE + ": " + contentType);
336                                            servletOutputStream.println(
337                                                    HttpHeaders.CONTENT_RANGE + ": " +
338                                                            range.getContentRange());
339                                            servletOutputStream.println();
340    
341                                            inputStream = copyRange(
342                                                    inputStream, outputStream, range.getStart(),
343                                                    range.getLength());
344                                    }
345    
346                                    servletOutputStream.println();
347                                    servletOutputStream.println(
348                                            StringPool.DOUBLE_DASH + boundary + StringPool.DOUBLE_DASH);
349                            }
350                    }
351                    finally {
352                            try {
353                                    inputStream.close();
354                            }
355                            catch (IOException ioe) {
356                            }
357                    }
358            }
359    
360            public static void write(
361                            HttpServletResponse response,
362                            BufferCacheServletResponse bufferCacheServletResponse)
363                    throws IOException {
364    
365                    if (bufferCacheServletResponse.isByteMode()) {
366                            write(response, bufferCacheServletResponse.getByteBuffer());
367                    }
368                    else if (bufferCacheServletResponse.isCharMode()) {
369                            write(response, bufferCacheServletResponse.getCharBuffer());
370                    }
371            }
372    
373            public static void write(HttpServletResponse response, byte[] bytes)
374                    throws IOException {
375    
376                    write(response, bytes, 0, 0);
377            }
378    
379            public static void write(
380                            HttpServletResponse response, byte[] bytes, int offset,
381                            int contentLength)
382                    throws IOException {
383    
384                    try {
385    
386                            // LEP-3122
387    
388                            if (!response.isCommitted()) {
389    
390                                    // LEP-536
391    
392                                    if (contentLength == 0) {
393                                            contentLength = bytes.length;
394                                    }
395    
396                                    response.setContentLength(contentLength);
397    
398                                    response.flushBuffer();
399    
400                                    if (response instanceof BufferCacheServletResponse) {
401                                            BufferCacheServletResponse bufferCacheServletResponse =
402                                                    (BufferCacheServletResponse)response;
403    
404                                            bufferCacheServletResponse.setByteBuffer(
405                                                    ByteBuffer.wrap(bytes, offset, contentLength));
406                                    }
407                                    else {
408                                            ServletOutputStream servletOutputStream =
409                                                    response.getOutputStream();
410    
411                                            if ((contentLength == 0) && ServerDetector.isJetty()) {
412                                            }
413                                            else {
414                                                    servletOutputStream.write(bytes, offset, contentLength);
415                                            }
416                                    }
417                            }
418                    }
419                    catch (IOException ioe) {
420                            if ((ioe instanceof SocketException) ||
421                                    isClientAbortException(ioe)) {
422    
423                                    if (_log.isWarnEnabled()) {
424                                            _log.warn(ioe);
425                                    }
426                            }
427                            else {
428                                    throw ioe;
429                            }
430                    }
431            }
432    
433            public static void write(HttpServletResponse response, byte[][] bytesArray)
434                    throws IOException {
435    
436                    try {
437    
438                            // LEP-3122
439    
440                            if (!response.isCommitted()) {
441                                    long contentLength = 0;
442    
443                                    for (byte[] bytes : bytesArray) {
444                                            contentLength += bytes.length;
445                                    }
446    
447                                    setContentLength(response, contentLength);
448    
449                                    response.flushBuffer();
450    
451                                    ServletOutputStream servletOutputStream =
452                                            response.getOutputStream();
453    
454                                    for (byte[] bytes : bytesArray) {
455                                            servletOutputStream.write(bytes);
456                                    }
457                            }
458                    }
459                    catch (IOException ioe) {
460                            if ((ioe instanceof SocketException) ||
461                                    isClientAbortException(ioe)) {
462    
463                                    if (_log.isWarnEnabled()) {
464                                            _log.warn(ioe);
465                                    }
466                            }
467                            else {
468                                    throw ioe;
469                            }
470                    }
471            }
472    
473            public static void write(
474                            HttpServletResponse response, ByteBuffer byteBuffer)
475                    throws IOException {
476    
477                    if (response instanceof BufferCacheServletResponse) {
478                            BufferCacheServletResponse bufferCacheServletResponse =
479                                    (BufferCacheServletResponse)response;
480    
481                            bufferCacheServletResponse.setByteBuffer(byteBuffer);
482                    }
483                    else {
484                            write(
485                                    response, byteBuffer.array(),
486                                    byteBuffer.arrayOffset() + byteBuffer.position(),
487                                    byteBuffer.arrayOffset() + byteBuffer.limit());
488                    }
489            }
490    
491            public static void write(
492                            HttpServletResponse response, CharBuffer charBuffer)
493                    throws IOException {
494    
495                    if (response instanceof BufferCacheServletResponse) {
496                            BufferCacheServletResponse bufferCacheServletResponse =
497                                    (BufferCacheServletResponse)response;
498    
499                            bufferCacheServletResponse.setCharBuffer(charBuffer);
500                    }
501                    else {
502                            ByteBuffer byteBuffer = CharsetEncoderUtil.encode(
503                                    StringPool.UTF8, charBuffer);
504    
505                            write(response, byteBuffer);
506                    }
507            }
508    
509            public static void write(HttpServletResponse response, File file)
510                    throws IOException {
511    
512                    if (response instanceof BufferCacheServletResponse) {
513                            BufferCacheServletResponse bufferCacheServletResponse =
514                                    (BufferCacheServletResponse)response;
515    
516                            ByteBuffer byteBuffer = ByteBuffer.wrap(FileUtil.getBytes(file));
517    
518                            bufferCacheServletResponse.setByteBuffer(byteBuffer);
519                    }
520                    else {
521                            FileInputStream fileInputStream = new FileInputStream(file);
522    
523                            try (FileChannel fileChannel = fileInputStream.getChannel()) {
524                                    long contentLength = fileChannel.size();
525    
526                                    setContentLength(response, contentLength);
527    
528                                    response.flushBuffer();
529    
530                                    fileChannel.transferTo(
531                                            0, contentLength,
532                                            Channels.newChannel(response.getOutputStream()));
533                            }
534                    }
535            }
536    
537            public static void write(
538                            HttpServletResponse response, InputStream inputStream)
539                    throws IOException {
540    
541                    write(response, inputStream, 0);
542            }
543    
544            public static void write(
545                            HttpServletResponse response, InputStream inputStream,
546                            long contentLength)
547                    throws IOException {
548    
549                    if (response.isCommitted()) {
550                            StreamUtil.cleanUp(inputStream);
551    
552                            return;
553                    }
554    
555                    if (contentLength > 0) {
556                            response.setHeader(
557                                    HttpHeaders.CONTENT_LENGTH, String.valueOf(contentLength));
558                    }
559    
560                    response.flushBuffer();
561    
562                    StreamUtil.transfer(inputStream, response.getOutputStream());
563            }
564    
565            public static void write(HttpServletResponse response, String s)
566                    throws IOException {
567    
568                    if (response instanceof BufferCacheServletResponse) {
569                            BufferCacheServletResponse bufferCacheServletResponse =
570                                    (BufferCacheServletResponse)response;
571    
572                            bufferCacheServletResponse.setString(s);
573                    }
574                    else {
575                            ByteBuffer byteBuffer = CharsetEncoderUtil.encode(
576                                    StringPool.UTF8, s);
577    
578                            write(response, byteBuffer);
579                    }
580            }
581    
582            protected static InputStream copyRange(
583                            InputStream inputStream, OutputStream outputStream, long start,
584                            long length)
585                    throws IOException {
586    
587                    if (inputStream instanceof FileInputStream) {
588                            FileInputStream fileInputStream = (FileInputStream)inputStream;
589    
590                            FileChannel fileChannel = fileInputStream.getChannel();
591    
592                            fileChannel.transferTo(
593                                    start, length, Channels.newChannel(outputStream));
594    
595                            return fileInputStream;
596                    }
597                    else if (inputStream instanceof ByteArrayInputStream) {
598                            ByteArrayInputStream byteArrayInputStream =
599                                    (ByteArrayInputStream)inputStream;
600    
601                            byteArrayInputStream.reset();
602    
603                            byteArrayInputStream.skip(start);
604    
605                            StreamUtil.transfer(byteArrayInputStream, outputStream, length);
606    
607                            return byteArrayInputStream;
608                    }
609                    else if (inputStream instanceof RandomAccessInputStream) {
610                            RandomAccessInputStream randomAccessInputStream =
611                                    (RandomAccessInputStream)inputStream;
612    
613                            randomAccessInputStream.seek(start);
614    
615                            StreamUtil.transfer(
616                                    randomAccessInputStream, outputStream, StreamUtil.BUFFER_SIZE,
617                                    false, length);
618    
619                            return randomAccessInputStream;
620                    }
621    
622                    return copyRange(
623                            new RandomAccessInputStream(inputStream), outputStream, start,
624                            length);
625            }
626    
627            protected static void setContentLength(
628                    HttpServletResponse response, long contentLength) {
629    
630                    response.setHeader(
631                            HttpHeaders.CONTENT_LENGTH, String.valueOf(contentLength));
632            }
633    
634            protected static void setHeaders(
635                    HttpServletRequest request, HttpServletResponse response,
636                    String fileName, String contentType, String contentDispositionType) {
637    
638                    if (_log.isDebugEnabled()) {
639                            _log.debug("Sending file of type " + contentType);
640                    }
641    
642                    // LEP-2201
643    
644                    if (Validator.isNotNull(contentType)) {
645                            if (contentType.equals(ContentTypes.IMAGE_X_MS_BMP) &&
646                                    BrowserSnifferUtil.isIe(request)) {
647    
648                                    contentType = ContentTypes.IMAGE_BMP;
649                            }
650    
651                            response.setContentType(contentType);
652                    }
653    
654                    if (!response.containsHeader(HttpHeaders.CACHE_CONTROL)) {
655                            response.setHeader(
656                                    HttpHeaders.CACHE_CONTROL,
657                                    HttpHeaders.CACHE_CONTROL_PRIVATE_VALUE);
658                    }
659    
660                    if (Validator.isNull(fileName)) {
661                            return;
662                    }
663    
664                    String contentDispositionFileName = "filename=\"" + fileName + "\"";
665    
666                    // If necessary for non-ASCII characters, encode based on RFC 2184.
667                    // However, not all browsers support RFC 2184. See LEP-3127.
668    
669                    boolean ascii = true;
670    
671                    for (int i = 0; i < fileName.length(); i++) {
672                            if (!Validator.isAscii(fileName.charAt(i))) {
673                                    ascii = false;
674    
675                                    break;
676                            }
677                    }
678    
679                    if (!ascii) {
680                            String encodedFileName = HttpUtil.encodeURL(fileName, true);
681    
682                            if (BrowserSnifferUtil.isIe(request)) {
683                                    contentDispositionFileName =
684                                            "filename=\"" + encodedFileName + "\"";
685                            }
686                            else {
687                                    contentDispositionFileName =
688                                            "filename*=UTF-8''" + encodedFileName;
689                            }
690                    }
691    
692                    if (Validator.isNull(contentDispositionType)) {
693                            String extension = GetterUtil.getString(
694                                    FileUtil.getExtension(fileName));
695    
696                            extension = StringUtil.toLowerCase(extension);
697    
698                            String[] mimeTypesContentDispositionInline = null;
699    
700                            try {
701                                    mimeTypesContentDispositionInline = PropsUtil.getArray(
702                                            PropsKeys.MIME_TYPES_CONTENT_DISPOSITION_INLINE);
703                            }
704                            catch (Exception e) {
705                                    mimeTypesContentDispositionInline = new String[0];
706                            }
707    
708                            if (ArrayUtil.contains(
709                                            mimeTypesContentDispositionInline, extension)) {
710    
711                                    contentDispositionType = HttpHeaders.CONTENT_DISPOSITION_INLINE;
712    
713                                    contentType = MimeTypesUtil.getContentType(fileName);
714    
715                                    response.setContentType(contentType);
716                            }
717                            else {
718                                    contentDispositionType =
719                                            HttpHeaders.CONTENT_DISPOSITION_ATTACHMENT;
720                            }
721                    }
722    
723                    StringBundler sb = new StringBundler(4);
724    
725                    sb.append(contentDispositionType);
726                    sb.append(StringPool.SEMICOLON);
727                    sb.append(StringPool.SPACE);
728                    sb.append(contentDispositionFileName);
729    
730                    if (_log.isDebugEnabled()) {
731                            _log.debug("Setting content disposition header " + sb.toString());
732                    }
733    
734                    response.setHeader(HttpHeaders.CONTENT_DISPOSITION, sb.toString());
735            }
736    
737            protected static void setHeaders(
738                    HttpServletRequest request, HttpServletResponse response,
739                    String fileName, String contentType, String contentDispositionType,
740                    Range range) {
741    
742                    setHeaders(
743                            request, response, fileName, contentType, contentDispositionType);
744    
745                    if (range != null) {
746                            response.setHeader(
747                                    HttpHeaders.CONTENT_RANGE, range.getContentRange());
748    
749                            response.setHeader(
750                                    HttpHeaders.CONTENT_LENGTH, String.valueOf(range.getLength()));
751                    }
752            }
753    
754            private static final String _CLIENT_ABORT_EXCEPTION =
755                    "org.apache.catalina.connector.ClientAbortException";
756    
757            private static final int _MAX_RANGE_FIELDS = GetterUtil.getInteger(
758                    PropsUtil.get(PropsKeys.WEB_SERVER_SERVLET_MAX_RANGE_FIELDS));
759    
760            private static final String _RANGE_REGEX =
761                    "^bytes=\\d*-\\d*(,\\s?\\d*-\\d*)*$";
762    
763            private static final Log _log = LogFactoryUtil.getLog(
764                    ServletResponseUtil.class);
765    
766    }