/** * Copyright (c) 2000-present Liferay, Inc. All rights reserved. * * This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 2.1 of the License, or (at your option) * any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. */ package com.liferay.portal.kernel.servlet; import com.liferay.portal.kernel.util.ArrayUtil; import com.liferay.portal.kernel.util.FileUtil; import com.liferay.portal.kernel.util.Props; import com.liferay.portal.kernel.util.PropsKeys; import com.liferay.portal.kernel.util.PropsUtil; import com.liferay.portal.kernel.util.StringPool; import com.liferay.portal.kernel.util.StringUtil; import com.liferay.portal.kernel.util.Validator; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Matchers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.powermock.api.mockito.PowerMockito; import org.powermock.modules.junit4.PowerMockRunner; import org.springframework.mock.web.MockHttpServletResponse; /** * @author Tomas Polesovsky */ @RunWith(PowerMockRunner.class) public class ServletResponseUtilRangeTest extends PowerMockito { @Before public void setUp() { MockitoAnnotations.initMocks(this); setUpBrowserSniffer(); setUpFileUtil(); setUpPropsUtil(); } @Test public void testGetMultipleRanges() throws IOException { setUpRange(_request, "bytes=1-3,3-8,9-11,12-12,30-"); List<Range> ranges = ServletResponseUtil.getRanges( _request, _response, 1000); Assert.assertEquals(ranges.toString(), 5, ranges.size()); assertRange(ranges.get(0), 1, 3, 3); assertRange(ranges.get(1), 3, 8, 6); assertRange(ranges.get(2), 9, 11, 3); assertRange(ranges.get(3), 12, 12, 1); assertRange(ranges.get(4), 30, 999, 970); } @Test public void testGetRangesPerSpec() throws IOException { // https://tools.ietf.org/html/rfc7233#section-2.1 long length = 10000; // The final 500 bytes (byte offsets 9500-9999, inclusive): bytes=-500 // or bytes=9500- setUpRange(_request, "bytes=-500"); List<Range> ranges = ServletResponseUtil.getRanges( _request, _response, length); Assert.assertEquals(ranges.toString(), 1, ranges.size()); assertRange(ranges.get(0), 9500, 9999, 500); setUpRange(_request, "bytes=9500-"); ranges = ServletResponseUtil.getRanges(_request, _response, length); Assert.assertEquals(ranges.toString(), 1, ranges.size()); assertRange(ranges.get(0), 9500, 9999, 500); // The first and last bytes only (bytes 0 and 9999): bytes=0-0,-1 setUpRange(_request, "bytes=0-0,-1"); ranges = ServletResponseUtil.getRanges(_request, _response, length); Assert.assertEquals(ranges.toString(), 2, ranges.size()); assertRange(ranges.get(0), 0, 0, 1); assertRange(ranges.get(1), 9999, 9999, 1); // Other valid (but not canonical) specifications of the second 500 // bytes (byte offsets 500-999, inclusive): bytes=500-600,601-999 or // bytes=500-700,601-999 setUpRange(_request, "bytes=500-600,601-999"); ranges = ServletResponseUtil.getRanges(_request, _response, length); Assert.assertEquals(ranges.toString(), 2, ranges.size()); assertRange(ranges.get(0), 500, 600, 101); assertRange(ranges.get(1), 601, 999, 399); setUpRange(_request, "bytes=500-700,601-999"); ranges = ServletResponseUtil.getRanges(_request, _response, length); Assert.assertEquals(ranges.toString(), 2, ranges.size()); assertRange(ranges.get(0), 500, 700, 201); assertRange(ranges.get(1), 601, 999, 399); } @Test public void testGetRangesSimple() throws IOException { setUpRange(_request, "bytes=0-999"); List<Range> ranges = ServletResponseUtil.getRanges( _request, _response, 1000); Assert.assertEquals(ranges.toString(), 1, ranges.size()); assertRange(ranges.get(0), 0, 999, 1000); } @Test public void testWriteWithRanges() throws IOException { byte[] content = new byte[1000]; Arrays.fill(content, (byte)48); testWriteWith(new ByteArrayInputStream(content), content); File tempFile = FileUtil.createTempFile(); try { try (FileOutputStream fos = new FileOutputStream(tempFile)) { fos.write(content); } testWriteWith(new FileInputStream(tempFile), content); } finally { tempFile.delete(); } testWriteWith( new BufferedInputStream(new ByteArrayInputStream(content)), content); } protected void assertRange(Range range, long start, long end, long length) { Assert.assertEquals(range.getStart(), start); Assert.assertEquals(range.getEnd(), end); Assert.assertEquals(range.getLength(), length); } protected void setUpBrowserSniffer() { BrowserSnifferUtil browserSnifferUtil = new BrowserSnifferUtil(); browserSnifferUtil.setBrowserSniffer(_browserSniffer); when( _browserSniffer.isIe(Matchers.any(HttpServletRequest.class)) ).thenReturn( false ); } protected void setUpFileUtil() { FileUtil fileUtil = new FileUtil(); fileUtil.setFile(_file); when( _file.createTempFile() ).thenAnswer( new Answer<File>() { @Override public File answer(InvocationOnMock invocation) throws Throwable { String name = String.valueOf(System.currentTimeMillis()); return File.createTempFile(name, null); } } ); when( _file.delete(Matchers.any(File.class)) ).thenAnswer( new Answer<Boolean>() { @Override public Boolean answer(InvocationOnMock invocation) throws Throwable { Object[] args = invocation.getArguments(); File file = (File)args[0]; return file.delete(); } } ); } protected void setUpPropsUtil() { PropsUtil.setProps(_props); when( _props.get(PropsKeys.WEB_SERVER_SERVLET_MAX_RANGE_FIELDS) ).thenReturn( "10" ); } protected void setUpRange(HttpServletRequest request, String rangeHeader) { when( request.getHeader(HttpHeaders.RANGE) ).thenReturn( rangeHeader ); } protected void testWriteWith(InputStream inputStream, byte[] content) throws IOException { setUpRange(_request, "bytes=0-9,980-989,980-999,990-999"); List<Range> ranges = ServletResponseUtil.getRanges( _request, _response, content.length); MockHttpServletResponse mockHttpServletResponse = new MockHttpServletResponse(); mockHttpServletResponse.setCharacterEncoding(StringPool.UTF8); ServletResponseUtil.write( _request, mockHttpServletResponse, "filename.txt", ranges, inputStream, content.length, "text/plain"); String contentType = mockHttpServletResponse.getContentType(); Assert.assertTrue( contentType.startsWith(_CONTENT_TYPE_BOUNDARY_PREFACE)); String boundary = contentType.substring( _CONTENT_TYPE_BOUNDARY_PREFACE.length()); String responseBody = mockHttpServletResponse.getContentAsString(); Assert.assertTrue( responseBody.startsWith("\r\n--" + boundary + "\r\n")); Assert.assertTrue(responseBody.endsWith("--" + boundary + "--\r\n")); String[] responseBodies = StringUtil.split(responseBody, boundary); Assert.assertEquals( Arrays.toString(responseBodies), ranges.size() + 2, responseBodies.length); Assert.assertEquals(StringPool.DOUBLE_DASH, responseBodies[0]); Assert.assertEquals( StringPool.DOUBLE_DASH, responseBodies[ranges.size() + 1]); for (int i = 0; i < ranges.size(); i++) { Range range = ranges.get(i); String[] lines = StringUtil.split( responseBodies[i + 1], StringPool.RETURN_NEW_LINE); Assert.assertEquals("Content-Type: text/plain", lines[0]); Assert.assertEquals( "Content-Range: " + range.getContentRange(), lines[1]); Assert.assertTrue(Validator.isNull(lines[2])); int start = (int)range.getStart(); int end = (int)range.getEnd(); byte[] bytes = ArrayUtil.subset(content, start, end + 1); Assert.assertArrayEquals(bytes, lines[3].getBytes("UTF-8")); Assert.assertEquals(StringPool.DOUBLE_DASH, lines[4]); } } private static final String _CONTENT_TYPE_BOUNDARY_PREFACE = "multipart/byteranges; boundary="; @Mock private BrowserSniffer _browserSniffer; @Mock private com.liferay.portal.kernel.util.File _file; @Mock private Props _props; @Mock private HttpServletRequest _request; @Mock private HttpServletResponse _response; }