blob: 918e55bb48af30ed315f765081d7eee9b1c76bf7 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005, 2014 Cognos Incorporated, IBM Corporation and others
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Cognos Incorporated - initial API and implementation
* IBM Corporation - bug fixes and enhancements
* Raymond Augé <raymond.auge@liferay.com> - Bug 436698
* Falko Schumann <falko.schumann@bitctrl.de> - Bug 519955
*******************************************************************************/
package org.eclipse.equinox.http.servlet.internal.servlet;
import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.security.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.*;
import org.eclipse.equinox.http.servlet.RangeAwareServletContextHelper;
import org.eclipse.equinox.http.servlet.internal.util.Const;
import org.osgi.service.http.context.ServletContextHelper;
public class ResourceServlet extends HttpServlet {
private static final long serialVersionUID = 3586876493076122102L;
private static final String LAST_MODIFIED = "Last-Modified"; //$NON-NLS-1$
private static final String IF_MODIFIED_SINCE = "If-Modified-Since"; //$NON-NLS-1$
private static final String IF_NONE_MATCH = "If-None-Match"; //$NON-NLS-1$
private static final String ETAG = "ETag"; //$NON-NLS-1$
private static final String RANGE = "Range"; //$NON-NLS-1$
private static final String ACCEPT_RANGES = "Accept-Ranges"; //$NON-NLS-1$
private static final String RANGE_UNIT_BYTES = "bytes"; //$NON-NLS-1$
private static final String CONTENT_RANGE = "Content-Range"; //$NON-NLS-1$
private final String internalName;
final ServletContextHelper servletContextHelper;
private final AccessControlContext acc;
public ResourceServlet(String internalName, ServletContextHelper servletContextHelper, AccessControlContext acc) {
if (internalName.equals(Const.SLASH)) {
internalName = Const.BLANK;
}
this.internalName = internalName;
this.servletContextHelper = servletContextHelper;
this.acc = acc;
}
public void service(HttpServletRequest req, final HttpServletResponse resp) throws IOException {
String method = req.getMethod();
if (method.equals("GET") || method.equals("POST") || method.equals("HEAD")) { //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
String pathInfo = HttpServletRequestWrapperImpl.getDispatchPathInfo(req);
if (pathInfo == null)
pathInfo = Const.BLANK;
String resourcePath = internalName + pathInfo;
URL resourceURL = servletContextHelper.getResource(resourcePath);
if (resourceURL != null)
writeResource(req, resp, resourcePath, resourceURL);
else
resp.sendError(HttpServletResponse.SC_NOT_FOUND, "ProxyServlet: " + req.getRequestURI()); //$NON-NLS-1$
} else {
resp.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
}
}
private void writeResource(final HttpServletRequest req, final HttpServletResponse resp, final String resourcePath, final URL resourceURL) throws IOException {
try {
AccessController.doPrivileged(new PrivilegedExceptionAction<Boolean>() {
@Override
public Boolean run() throws Exception {
URLConnection connection = resourceURL.openConnection();
long lastModified = connection.getLastModified();
int contentLength = connection.getContentLength();
String etag = null;
if (lastModified != -1 && contentLength != -1)
etag = "W/\"" + contentLength + "-" + lastModified + "\""; //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
// Check for cache revalidation.
// We should prefer ETag validation as the guarantees are stronger and all HTTP 1.1 clients should be using it
String ifNoneMatch = req.getHeader(IF_NONE_MATCH);
if (ifNoneMatch != null && etag != null && ifNoneMatch.indexOf(etag) != -1) {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return Boolean.TRUE;
}
long ifModifiedSince = req.getDateHeader(IF_MODIFIED_SINCE);
// for purposes of comparison we add 999 to ifModifiedSince since the fidelity
// of the IMS header generally doesn't include milli-seconds
if (ifModifiedSince > -1 && lastModified > 0 && lastModified <= (ifModifiedSince + 999)) {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return Boolean.TRUE;
}
String rangeHeader = req.getHeader(RANGE);
Range range = null;
if (rangeHeader != null) {
range = Range.createFromRangeHeader(rangeHeader);
range.completeLength = contentLength;
range.updateBytePos();
if (!range.isValid()) {
resp.setHeader(ACCEPT_RANGES, RANGE_UNIT_BYTES);
resp.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return Boolean.TRUE;
}
}
// return the full contents regularly
if (contentLength != -1)
resp.setContentLength(contentLength);
String filename = new File(resourcePath).getName();
String contentType = servletContextHelper.getMimeType(filename);
if (contentType == null)
contentType = getServletConfig().getServletContext().getMimeType(filename);
if (contentType != null)
resp.setContentType(contentType);
if (lastModified > 0)
resp.setDateHeader(LAST_MODIFIED, lastModified);
if (etag != null)
resp.setHeader(ETAG, etag);
if (range == null &&
(servletContextHelper instanceof RangeAwareServletContextHelper) &&
((RangeAwareServletContextHelper)servletContextHelper).rangeableContentType(contentType, req.getHeader("User-Agent"))) { //$NON-NLS-1$
range = new Range();
range.firstBytePos = 0;
range.completeLength = contentLength;
range.updateBytePos();
}
if (range != null) {
resp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
resp.setHeader(ACCEPT_RANGES, RANGE_UNIT_BYTES);
resp.setContentLength(range.contentLength());
resp.setHeader(CONTENT_RANGE, RANGE_UNIT_BYTES + " " + range.firstBytePos + "-" + range.lastBytePos + "/" + range.completeLength); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
if (contentLength != 0) {
// open the input stream
try (InputStream is = connection.getInputStream()) {
// write the resource
try {
OutputStream os = resp.getOutputStream();
int writtenContentLength = writeResourceToOutputStream(is, os, range);
if (contentLength == -1 || contentLength != writtenContentLength)
resp.setContentLength(writtenContentLength);
} catch (IllegalStateException e) { // can occur if the response output is already open as a Writer
Writer writer = resp.getWriter();
writeResourceToWriter(is, writer, range);
// Since ContentLength is a measure of the number of bytes contained in the body
// of a message when we use a Writer we lose control of the exact byte count and
// defer the problem to the Servlet Engine's Writer implementation.
}
} catch (FileNotFoundException e) {
// FileNotFoundException may indicate the following scenarios
// - url is a directory
// - url is not accessible
sendError(resp, HttpServletResponse.SC_FORBIDDEN);
} catch (SecurityException e) {
// SecurityException may indicate the following scenarios
// - url is not accessible
sendError(resp, HttpServletResponse.SC_FORBIDDEN);
}
}
return Boolean.TRUE;
}
}, acc);
} catch (PrivilegedActionException e) {
throw (IOException) e.getException();
}
}
void sendError(final HttpServletResponse resp, int sc) throws IOException {
try {
// we need to reset headers for 302 and 403
resp.reset();
resp.sendError(sc);
} catch (IllegalStateException e) {
// this could happen if the response has already been committed
}
}
int writeResourceToOutputStream(InputStream is, OutputStream os, Range range) throws IOException {
if (range != null) {
if (range.firstBytePos != Range.NOT_SET) {
is.skip(range.firstBytePos);
} else {
is.skip(range.completeLength - range.lastBytePos);
}
}
byte[] buffer = new byte[8192];
int bytesRead = is.read(buffer);
int writtenContentLength = 0;
while (bytesRead != -1 && (range == null || range.lastBytePos == Range.NOT_SET || writtenContentLength < range.lastBytePos)) {
if (range != null && range.lastBytePos != Range.NOT_SET && (bytesRead + writtenContentLength) > range.lastBytePos) {
bytesRead = range.contentLength() - writtenContentLength;
}
os.write(buffer, 0, bytesRead);
writtenContentLength += bytesRead;
bytesRead = is.read(buffer);
}
return writtenContentLength;
}
void writeResourceToWriter(InputStream is, Writer writer, Range range) throws IOException {
if (range != null) {
if (range.firstBytePos != Range.NOT_SET) {
is.skip(range.firstBytePos);
} else {
is.skip(range.completeLength - range.lastBytePos);
}
}
try (Reader reader = new InputStreamReader(is)) {
char[] buffer = new char[8192];
int charsRead = reader.read(buffer);
int writtenContentLength = 0;
while (charsRead != -1 && (range == null || range.lastBytePos == Range.NOT_SET || writtenContentLength < range.lastBytePos)) {
if (range != null && range.lastBytePos != Range.NOT_SET && (charsRead + writtenContentLength) > range.lastBytePos) {
charsRead = range.contentLength() - writtenContentLength;
}
writer.write(buffer, 0, charsRead);
writtenContentLength += charsRead;
charsRead = reader.read(buffer);
}
}
}
static class Range {
private static final Pattern RANGE_PATTERN = Pattern.compile("^(.+)=(\\d+)?-(\\d+)?$"); //$NON-NLS-1$
static final int NOT_SET = -1;
String rangeUnit = RANGE_UNIT_BYTES;
int firstBytePos = NOT_SET;
int lastBytePos = NOT_SET;
int completeLength = NOT_SET;
static Range createFromRangeHeader(String header) {
Range range = new Range();
Matcher matcher = RANGE_PATTERN.matcher(header);
if (!matcher.matches()) {
// use default values
return range;
}
range.rangeUnit = matcher.group(1);
if (matcher.group(2) != null) {
try {
range.firstBytePos = Integer.parseInt(matcher.group(2));
} catch (NumberFormatException ignored) {
// use default value
}
}
if (matcher.group(3) != null) {
try {
range.lastBytePos = Integer.parseInt(matcher.group(3));
} catch (NumberFormatException ignored) {
// use default value
}
}
return range;
}
void updateBytePos() {
if (lastBytePos == -1 || lastBytePos >= completeLength) {
lastBytePos = completeLength - 1;
}
if (firstBytePos == -1) {
firstBytePos = completeLength - lastBytePos - 1;
lastBytePos = completeLength - 1;
}
}
boolean isValid() {
// we accept bytes unit only
if (!RANGE_UNIT_BYTES.equals(rangeUnit)) {
return false;
}
if (firstBytePos == NOT_SET && lastBytePos == NOT_SET) {
return false;
}
if (firstBytePos >= completeLength) {
return false;
}
if (lastBytePos >= completeLength) {
return false;
}
return true;
}
int contentLength() {
return lastBytePos - firstBytePos + 1;
}
}
}