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