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.setContentLength((int)contentLength);
567                    }
568    
569                    response.flushBuffer();
570    
571                    StreamUtil.transfer(inputStream, response.getOutputStream());
572            }
573    
574            public static void write(HttpServletResponse response, String s)
575                    throws IOException {
576    
577                    if (response instanceof BufferCacheServletResponse) {
578                            BufferCacheServletResponse bufferCacheServletResponse =
579                                    (BufferCacheServletResponse)response;
580    
581                            bufferCacheServletResponse.setString(s);
582                    }
583                    else {
584                            ByteBuffer byteBuffer = CharsetEncoderUtil.encode(
585                                    StringPool.UTF8, s);
586    
587                            write(response, byteBuffer);
588                    }
589            }
590    
591            protected static void copyRange(
592                            InputStream inputStream, OutputStream outputStream, long start,
593                            long length)
594                    throws IOException {
595    
596                    if (inputStream instanceof FileInputStream) {
597                            FileInputStream fileInputStream = (FileInputStream)inputStream;
598    
599                            FileChannel fileChannel = fileInputStream.getChannel();
600    
601                            fileChannel.transferTo(
602                                    start, length, Channels.newChannel(outputStream));
603                    }
604                    else if (inputStream instanceof ByteArrayInputStream) {
605                            ByteArrayInputStream byteArrayInputStream =
606                                    (ByteArrayInputStream)inputStream;
607    
608                            byteArrayInputStream.skip(start);
609    
610                            StreamUtil.transfer(byteArrayInputStream, outputStream, length);
611                    }
612                    else {
613                            RandomAccessInputStream randomAccessInputStream =
614                                    new RandomAccessInputStream(inputStream);
615    
616                            randomAccessInputStream.seek(start);
617    
618                            StreamUtil.transfer(randomAccessInputStream, outputStream, length);
619                    }
620            }
621    
622            protected static void setHeaders(
623                    HttpServletRequest request, HttpServletResponse response,
624                    String fileName, String contentType, String contentDispositionType) {
625    
626                    if (_log.isDebugEnabled()) {
627                            _log.debug("Sending file of type " + contentType);
628                    }
629    
630                    // LEP-2201
631    
632                    if (Validator.isNotNull(contentType)) {
633                            response.setContentType(contentType);
634                    }
635    
636                    if (!response.containsHeader(HttpHeaders.CACHE_CONTROL)) {
637                            response.setHeader(
638                                    HttpHeaders.CACHE_CONTROL,
639                                    HttpHeaders.CACHE_CONTROL_PRIVATE_VALUE);
640                    }
641    
642                    if (Validator.isNull(fileName)) {
643                            return;
644                    }
645    
646                    String contentDispositionFileName = "filename=\"" + fileName + "\"";
647    
648                    // If necessary for non-ASCII characters, encode based on RFC 2184.
649                    // However, not all browsers support RFC 2184. See LEP-3127.
650    
651                    boolean ascii = true;
652    
653                    for (int i = 0; i < fileName.length(); i++) {
654                            if (!Validator.isAscii(fileName.charAt(i))) {
655                                    ascii = false;
656    
657                                    break;
658                            }
659                    }
660    
661                    if (!ascii) {
662                            String encodedFileName = HttpUtil.encodeURL(fileName, true);
663    
664                            if (BrowserSnifferUtil.isIe(request)) {
665                                    contentDispositionFileName =
666                                            "filename=\"" + encodedFileName + "\"";
667                            }
668                            else {
669                                    contentDispositionFileName =
670                                            "filename*=UTF-8''" + encodedFileName;
671                            }
672                    }
673    
674                    if (Validator.isNull(contentDispositionType)) {
675                            String extension = GetterUtil.getString(
676                                    FileUtil.getExtension(fileName));
677    
678                            extension = StringUtil.toLowerCase(extension);
679    
680                            String[] mimeTypesContentDispositionInline = null;
681    
682                            try {
683                                    mimeTypesContentDispositionInline = PropsUtil.getArray(
684                                            PropsKeys.MIME_TYPES_CONTENT_DISPOSITION_INLINE);
685                            }
686                            catch (Exception e) {
687                                    mimeTypesContentDispositionInline = new String[0];
688                            }
689    
690                            if (ArrayUtil.contains(
691                                            mimeTypesContentDispositionInline, extension)) {
692    
693                                    contentDispositionType = HttpHeaders.CONTENT_DISPOSITION_INLINE;
694    
695                                    contentType = MimeTypesUtil.getContentType(fileName);
696    
697                                    response.setContentType(contentType);
698                            }
699                            else {
700                                    contentDispositionType =
701                                            HttpHeaders.CONTENT_DISPOSITION_ATTACHMENT;
702                            }
703                    }
704    
705                    StringBundler sb = new StringBundler(4);
706    
707                    sb.append(contentDispositionType);
708                    sb.append(StringPool.SEMICOLON);
709                    sb.append(StringPool.SPACE);
710                    sb.append(contentDispositionFileName);
711    
712                    if (_log.isDebugEnabled()) {
713                            _log.debug("Setting content disposition header " + sb.toString());
714                    }
715    
716                    response.setHeader(HttpHeaders.CONTENT_DISPOSITION, sb.toString());
717            }
718    
719            protected static void setHeaders(
720                    HttpServletRequest request, HttpServletResponse response,
721                    String fileName, String contentType, String contentDispositionType,
722                    Range range) {
723    
724                    setHeaders(
725                            request, response, fileName, contentType, contentDispositionType);
726    
727                    if (range != null) {
728                            response.setHeader(
729                                    HttpHeaders.CONTENT_RANGE, range.getContentRange());
730    
731                            response.setHeader(
732                                    HttpHeaders.CONTENT_LENGTH, String.valueOf(range.getLength()));
733                    }
734            }
735    
736            private static final String _CLIENT_ABORT_EXCEPTION =
737                    "org.apache.catalina.connector.ClientAbortException";
738    
739            private static final int _MAX_RANGE_FIELDS = GetterUtil.getInteger(
740                    PropsUtil.get(PropsKeys.WEB_SERVER_SERVLET_MAX_RANGE_FIELDS));
741    
742            private static final String _RANGE_REGEX =
743                    "^bytes=\\d*-\\d*(,\\s?\\d*-\\d*)*$";
744    
745            private static Log _log = LogFactoryUtil.getLog(ServletResponseUtil.class);
746    
747    }