// Jetty8YaCyDefaultServlet
// ------------------------
// Copyright 2013 by Michael Peter Christen; mc@yacy.net, Frankfurt a. M., Germany
// First released 2013 at http://yacy.net
//
// $LastChangedDate$
// $LastChangedRevision$
// $LastChangedBy$
//
// 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.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program in the file lgpl21.txt
// If not, see .
//
package net.yacy.http;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.List;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.yacy.cora.protocol.HeaderFramework;
import net.yacy.cora.util.ConcurrentLog;
import net.yacy.kelondro.util.FileUtils;
import org.eclipse.jetty.http.HttpContent;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeaders;
import org.eclipse.jetty.http.HttpMethods;
import org.eclipse.jetty.io.Buffer;
import org.eclipse.jetty.io.WriterOutputStream;
import org.eclipse.jetty.server.AbstractHttpConnection;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpOutput;
import org.eclipse.jetty.server.InclusiveByteRange;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.nio.NIOConnector;
import org.eclipse.jetty.server.ssl.SslConnector;
import org.eclipse.jetty.util.MultiPartOutputStream;
import org.eclipse.jetty.util.QuotedStringTokenizer;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
/* ------------------------------------------------------------ */
/**
* YaCyDefaultServlet base on Jetty DefaultServlet.java
* handles static files and the YaCy servlets.
*/
/**
* The default servlet. This servlet, normally mapped to /, provides the
* handling for static content, OPTION and TRACE methods for the context. The
* following initParameters are supported, these can be set either on the
* servlet itself or as ServletContext initParameters :
*
* acceptRanges If true, range requests and responses are
* supported
*
* dirAllowed If true, directory listings are returned if no
* welcome file is found. Else 403 Forbidden.
*
* welcomeFile name of the welcome file (default is "index.html", "welcome.html")
*
* gzip If set to true, then static content will be served as
* gzip content encoded if a matching resource is
* found ending with ".gz"
*
* resourceBase Set to replace the context resource base
*
* resourceCache If set, this is a context attribute name, which the servlet
* will use to look for a shared ResourceCache instance.
*
* relativeResourceBase
* Set with a pathname relative to the base of the
* servlet context root. Useful for only serving static content out
* of only specific subdirectories.
*
* pathInfoOnly If true, only the path info will be applied to the resourceBase
*
*
* etags If True, weak etags will be generated and handled.
*
*
*
*/
public class Jetty8YaCyDefaultServlet extends YaCyDefaultServlet implements ResourceFactory {
private static final long serialVersionUID = 4900000000000001110L;
/* ------------------------------------------------------------ */
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String servletPath = null;
String pathInfo = null;
Enumeration reqRanges = null;
boolean included = request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null;
if (included) {
servletPath = (String) request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
pathInfo = (String) request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
if (servletPath == null) {
servletPath = request.getServletPath();
pathInfo = request.getPathInfo();
}
} else {
servletPath = _pathInfoOnly ? "/" : request.getServletPath();
pathInfo = request.getPathInfo();
// Is this a Range request?
reqRanges = request.getHeaders(HeaderFramework.RANGE);
if (!hasDefinedRange(reqRanges)) {
reqRanges = null;
}
}
if (pathInfo.startsWith("/currentyacypeer/")) pathInfo = pathInfo.substring(16);
String pathInContext = URIUtil.addPaths(servletPath, pathInfo);
boolean endsWithSlash = (pathInfo == null ? request.getServletPath() : pathInfo).endsWith(URIUtil.SLASH);
// Find the resource and content
Resource resource = null;
HttpContent content = null;
try {
// is gzip enabled?
String pathInContextGz = null;
boolean gzip = false;
if (!included && _gzip && reqRanges == null && !endsWithSlash) {
// Look for a gzip resource
pathInContextGz = pathInContext + ".gz";
resource = getResource(pathInContextGz);
// Does a gzip resource exist?
if (resource != null && resource.exists() && !resource.isDirectory()) {
// Tell caches that response may vary by accept-encoding
response.addHeader(HttpHeaders.VARY, HeaderFramework.ACCEPT_ENCODING);
// Does the client accept gzip?
String accept = request.getHeader(HeaderFramework.ACCEPT_ENCODING);
if (accept != null && accept.indexOf(HeaderFramework.CONTENT_ENCODING_GZIP) >= 0) {
gzip = true;
}
}
}
// find resource
if (!gzip) resource = getResource(pathInContext);
// Look for a class resource
boolean hasClass = false;
if (reqRanges == null && !endsWithSlash) {
final int p = pathInContext.lastIndexOf('.');
if (p >= 0) {
String pathofClass = pathInContext.substring(0, p) + ".class";
Resource classresource = _resourceBase.addPath(pathofClass);
// Does a class resource exist?
if (classresource != null && classresource.exists() && !classresource.isDirectory()) {
hasClass = true;
}
}
}
if (ConcurrentLog.isFine("FILEHANDLER")) {
ConcurrentLog.fine("FILEHANDLER","YaCyDefaultServlet: uri=" + request.getRequestURI() + " resource=" + resource + (content != null ? " content" : ""));
}
// Handle resource
if (!hasClass && (resource == null || !resource.exists())) {
if (included) {
throw new FileNotFoundException("!" + pathInContext);
}
response.sendError(HttpServletResponse.SC_NOT_FOUND);
} else if (!resource.isDirectory()) {
if (endsWithSlash && pathInContext.length() > 1) {
String q = request.getQueryString();
pathInContext = pathInContext.substring(0, pathInContext.length() - 1);
if (q != null && q.length() != 0) {
pathInContext += "?" + q;
}
response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(_servletContext.getContextPath(), pathInContext)));
} else {
// ensure we have content
if (content == null) {
content = new HttpContent.ResourceAsHttpContent(resource, _mimeTypes.getMimeByExtension(resource.toString()), response.getBufferSize(), _etags);
}
if (hasClass) { // this is a YaCy servlet, handle the template
handleTemplate(pathInfo, request, response);
} else {
if (included || passConditionalHeaders(request, response, resource, content)) {
if (gzip) {
response.setHeader(HeaderFramework.CONTENT_ENCODING, HeaderFramework.CONTENT_ENCODING_GZIP);
String mt = _servletContext.getMimeType(pathInContext);
if (mt != null) {
response.setContentType(mt);
}
}
sendData(request, response, included, resource, content, reqRanges);
}
}
}
} else {
String welcome = null;
if (!endsWithSlash || (pathInContext.length() == 1 && request.getAttribute("org.eclipse.jetty.server.nullPathInfo") != null)) {
StringBuffer buf = request.getRequestURL();
synchronized (buf) {
int param = buf.lastIndexOf(";");
if (param < 0) {
buf.append('/');
} else {
buf.insert(param, '/');
}
String q = request.getQueryString();
if (q != null && q.length() != 0) {
buf.append('?');
buf.append(q);
}
response.setContentLength(0);
response.sendRedirect(response.encodeRedirectURL(buf.toString()));
}
} // else look for a welcome file
else if (null != (welcome = getWelcomeFile(pathInContext))) {
ConcurrentLog.fine("FILEHANDLER","welcome={}" + welcome);
// Forward to the index
RequestDispatcher dispatcher = request.getRequestDispatcher(welcome);
if (dispatcher != null) {
if (included) {
dispatcher.include(request, response);
} else {
request.setAttribute("org.eclipse.jetty.server.welcome", welcome);
dispatcher.forward(request, response);
}
}
} else {
content = new HttpContent.ResourceAsHttpContent(resource, _mimeTypes.getMimeByExtension(resource.toString()), _etags);
if (included || passConditionalHeaders(request, response, resource, content)) {
sendDirectory(request, response, resource, pathInContext);
}
}
}
} catch (IllegalArgumentException e) {
ConcurrentLog.logException(e);
if (!response.isCommitted()) {
response.sendError(500, e.getMessage());
}
} finally {
if (content != null) {
content.release();
} else if (resource != null) {
resource.release();
}
}
}
/* ------------------------------------------------------------ */
/* Check modification date headers.
*/
@Override
protected boolean passConditionalHeaders(HttpServletRequest request, HttpServletResponse response, Resource resource, HttpContent content)
throws IOException {
try {
if (!request.getMethod().equals(HttpMethods.HEAD)) {
if (_etags) {
String ifm = request.getHeader(HttpHeaders.IF_MATCH);
if (ifm != null) {
boolean match = false;
if (content != null && content.getETag() != null) {
QuotedStringTokenizer quoted = new QuotedStringTokenizer(ifm, ", ", false, true);
while (!match && quoted.hasMoreTokens()) {
String tag = quoted.nextToken();
if (content.getETag().toString().equals(tag)) {
match = true;
}
}
}
if (!match) {
Response r = Response.getResponse(response);
r.reset(true);
r.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
return false;
}
}
String ifnm = request.getHeader(HttpHeaders.IF_NONE_MATCH);
if (ifnm != null && content != null && content.getETag() != null) {
// Look for GzipFiltered version of etag
if (content.getETag().toString().equals(request.getAttribute("o.e.j.s.GzipFilter.ETag"))) {
Response r = Response.getResponse(response);
r.reset(true);
r.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
r.getHttpFields().put(HttpHeaders.ETAG_BUFFER, ifnm);
return false;
}
// Handle special case of exact match.
if (content.getETag().toString().equals(ifnm)) {
Response r = Response.getResponse(response);
r.reset(true);
r.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
r.getHttpFields().put(HttpHeaders.ETAG_BUFFER, content.getETag());
return false;
}
// Handle list of tags
QuotedStringTokenizer quoted = new QuotedStringTokenizer(ifnm, ", ", false, true);
while (quoted.hasMoreTokens()) {
String tag = quoted.nextToken();
if (content.getETag().toString().equals(tag)) {
Response r = Response.getResponse(response);
r.reset(true);
r.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
r.getHttpFields().put(HttpHeaders.ETAG_BUFFER, content.getETag());
return false;
}
}
return true;
}
}
String ifms = request.getHeader(HttpHeaders.IF_MODIFIED_SINCE);
if (ifms != null) {
//Get jetty's Response impl
Response r = Response.getResponse(response);
if (content != null) {
Buffer mdlm = content.getLastModified();
if (mdlm != null) {
if (ifms.equals(mdlm.toString())) {
r.reset(true);
r.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
if (_etags) {
r.getHttpFields().add(HttpHeaders.ETAG_BUFFER, content.getETag());
}
r.flushBuffer();
return false;
}
}
}
long ifmsl = request.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE);
if (ifmsl != -1) {
if (resource.lastModified() / 1000 <= ifmsl / 1000) {
r.reset(true);
r.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
if (_etags) {
r.getHttpFields().add(HttpHeaders.ETAG_BUFFER, content.getETag());
}
r.flushBuffer();
return false;
}
}
}
// Parse the if[un]modified dates and compare to resource
long date = request.getDateHeader(HttpHeaders.IF_UNMODIFIED_SINCE);
if (date != -1) {
if (resource.lastModified() / 1000 > date / 1000) {
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
return false;
}
}
}
} catch (IllegalArgumentException iae) {
if (!response.isCommitted()) {
response.sendError(400, iae.getMessage());
}
throw iae;
}
return true;
}
/* ------------------------------------------------------------ */
@Override
protected void sendData(HttpServletRequest request,
HttpServletResponse response,
boolean include,
Resource resource,
HttpContent content,
Enumeration reqRanges)
throws IOException {
boolean direct;
long content_length;
if (content == null) {
direct = false;
content_length = resource.length();
} else {
Connector connector = AbstractHttpConnection.getCurrentConnection().getConnector();
direct = connector instanceof NIOConnector && ((NIOConnector) connector).getUseDirectBuffers() && !(connector instanceof SslConnector);
content_length = content.getContentLength();
}
// Get the output stream (or writer)
OutputStream out = null;
boolean written;
try {
out = response.getOutputStream();
// has a filter already written to the response?
written = out instanceof HttpOutput
? ((HttpOutput) out).isWritten()
: AbstractHttpConnection.getCurrentConnection().getGenerator().isWritten();
} catch (IllegalStateException e) {
out = new WriterOutputStream(response.getWriter());
written = true; // there may be data in writer buffer, so assume written
}
if (reqRanges == null || !reqRanges.hasMoreElements() || content_length < 0) {
// if there were no ranges, send entire entity
if (include) {
resource.writeTo(out, 0, content_length);
} else {
// See if a direct methods can be used?
if (content != null && !written && out instanceof HttpOutput) {
if (response instanceof Response) {
writeOptionHeaders(((Response) response).getHttpFields());
((AbstractHttpConnection.Output) out).sendContent(content);
} else {
Buffer buffer = direct ? content.getDirectBuffer() : content.getIndirectBuffer();
if (buffer != null) {
writeHeaders(response, content, content_length);
((AbstractHttpConnection.Output) out).sendContent(buffer);
} else {
writeHeaders(response, content, content_length);
resource.writeTo(out, 0, content_length);
}
}
} else {
// Write headers normally
writeHeaders(response, content, written ? -1 : content_length);
// Write content normally
Buffer buffer = (content == null) ? null : content.getIndirectBuffer();
if (buffer != null) {
buffer.writeTo(out);
} else {
resource.writeTo(out, 0, content_length);
}
}
}
} else {
// Parse the satisfiable ranges
List ranges = InclusiveByteRange.satisfiableRanges(reqRanges, content_length);
// if there are no satisfiable ranges, send 416 response
if (ranges == null || ranges.size() == 0) {
writeHeaders(response, content, content_length);
response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
response.setHeader(HttpHeaders.CONTENT_RANGE,
InclusiveByteRange.to416HeaderRangeString(content_length));
resource.writeTo(out, 0, content_length);
return;
}
// if there is only a single valid range (must be satisfiable
// since were here now), send that range with a 216 response
if (ranges.size() == 1) {
InclusiveByteRange singleSatisfiableRange =
(InclusiveByteRange) ranges.get(0);
long singleLength = singleSatisfiableRange.getSize(content_length);
writeHeaders(response, content, singleLength);
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
response.setHeader(HttpHeaders.CONTENT_RANGE,
singleSatisfiableRange.toHeaderRangeString(content_length));
resource.writeTo(out, singleSatisfiableRange.getFirst(content_length), singleLength);
return;
}
// multiple non-overlapping valid ranges cause a multipart
// 216 response which does not require an overall
// content-length header
//
writeHeaders(response, content, -1);
String mimetype = (content.getContentType() == null ? null : content.getContentType().toString());
if (mimetype == null) {
ConcurrentLog.warn("FILEHANDLER","YaCyDefaultServlet: Unknown mimetype for " + request.getRequestURI());
}
MultiPartOutputStream multi = new MultiPartOutputStream(out);
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
// If the request has a "Request-Range" header then we need to
// send an old style multipart/x-byteranges Content-Type. This
// keeps Netscape and acrobat happy. This is what Apache does.
String ctp;
if (request.getHeader(HttpHeaders.REQUEST_RANGE) != null) {
ctp = "multipart/x-byteranges; boundary=";
} else {
ctp = "multipart/byteranges; boundary=";
}
response.setContentType(ctp + multi.getBoundary());
InputStream in = resource.getInputStream();
long pos = 0;
// calculate the content-length
int length = 0;
String[] header = new String[ranges.size()];
for (int i = 0; i < ranges.size(); i++) {
InclusiveByteRange ibr = (InclusiveByteRange) ranges.get(i);
header[i] = ibr.toHeaderRangeString(content_length);
length +=
((i > 0) ? 2 : 0)
+ 2 + multi.getBoundary().length() + 2
+ (mimetype == null ? 0 : HeaderFramework.CONTENT_TYPE.length() + 2 + mimetype.length()) + 2
+ HeaderFramework.CONTENT_RANGE.length() + 2 + header[i].length() + 2
+ 2
+ (ibr.getLast(content_length) - ibr.getFirst(content_length)) + 1;
}
length += 2 + 2 + multi.getBoundary().length() + 2 + 2;
response.setContentLength(length);
for (int i = 0; i < ranges.size(); i++) {
InclusiveByteRange ibr = (InclusiveByteRange) ranges.get(i);
multi.startPart(mimetype, new String[]{HeaderFramework.CONTENT_RANGE + ": " + header[i]});
long start = ibr.getFirst(content_length);
long size = ibr.getSize(content_length);
if (in != null) {
// Handle non cached resource
if (start < pos) {
in.close();
in = resource.getInputStream();
pos = 0;
}
if (pos < start) {
in.skip(start - pos);
pos = start;
}
FileUtils.copy(in, multi, size);
pos += size;
} else // Handle cached resource
{
(resource).writeTo(multi, start, size);
}
}
if (in != null) {
in.close();
}
multi.close();
}
}
}