diff options
Diffstat (limited to 'jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java')
-rw-r--r-- | jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java | 781 |
1 files changed, 781 insertions, 0 deletions
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java new file mode 100644 index 0000000000..1dbaa0668f --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java @@ -0,0 +1,781 @@ +// +// ======================================================================== +// Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.server; + +import static org.eclipse.jetty.http.GzipHttpContent.ETAG_GZIP_QUOTE; +import static org.eclipse.jetty.http.GzipHttpContent.removeGzipFromETag; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.Enumeration; +import java.util.List; + +import javax.servlet.AsyncContext; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.DateParser; +import org.eclipse.jetty.http.HttpContent; +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.PreEncodedHttpField; +import org.eclipse.jetty.io.WriterOutputStream; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.MultiPartOutputStream; +import org.eclipse.jetty.util.QuotedStringTokenizer; +import org.eclipse.jetty.util.URIUtil; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.resource.Resource; + +/** + * Abstract resource service, used by DefaultServlet and ResourceHandler + * + */ +public abstract class ResourceService +{ + private static final Logger LOG = Log.getLogger(ResourceService.class); + + private static final PreEncodedHttpField ACCEPT_RANGES = new PreEncodedHttpField(HttpHeader.ACCEPT_RANGES, "bytes"); + + private HttpContent.Factory _contentFactory; + private boolean _acceptRanges=true; + private boolean _dirAllowed=true; + private boolean _redirectWelcome=false; + private boolean _gzip=false; + private boolean _pathInfoOnly=false; + private boolean _etags=false; + private HttpField _cacheControl; + private List<String> _gzipEquivalentFileExtensions; + + public HttpContent.Factory getContentFactory() + { + return _contentFactory; + } + + public void setContentFactory(HttpContent.Factory contentFactory) + { + _contentFactory = contentFactory; + } + + public boolean isAcceptRanges() + { + return _acceptRanges; + } + + public void setAcceptRanges(boolean acceptRanges) + { + _acceptRanges = acceptRanges; + } + + public boolean isDirAllowed() + { + return _dirAllowed; + } + + public void setDirAllowed(boolean dirAllowed) + { + _dirAllowed = dirAllowed; + } + + public boolean isRedirectWelcome() + { + return _redirectWelcome; + } + + public void setRedirectWelcome(boolean redirectWelcome) + { + _redirectWelcome = redirectWelcome; + } + + public boolean isGzip() + { + return _gzip; + } + + public void setGzip(boolean gzip) + { + _gzip = gzip; + } + + public boolean isPathInfoOnly() + { + return _pathInfoOnly; + } + + public void setPathInfoOnly(boolean pathInfoOnly) + { + _pathInfoOnly = pathInfoOnly; + } + + public boolean isEtags() + { + return _etags; + } + + public void setEtags(boolean etags) + { + _etags = etags; + } + + public HttpField getCacheControl() + { + return _cacheControl; + } + + public void setCacheControl(HttpField cacheControl) + { + _cacheControl = cacheControl; + } + + public List<String> getGzipEquivalentFileExtensions() + { + return _gzipEquivalentFileExtensions; + } + + public void setGzipEquivalentFileExtensions(List<String> gzipEquivalentFileExtensions) + { + _gzipEquivalentFileExtensions = gzipEquivalentFileExtensions; + } + + /* ------------------------------------------------------------ */ + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException + { + String servletPath=null; + String pathInfo=null; + Enumeration<String> reqRanges = null; + boolean included =request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI)!=null; + if (included) + { + servletPath= _pathInfoOnly?"/":(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(HttpHeader.RANGE.asString()); + if (!hasDefinedRange(reqRanges)) + reqRanges = null; + } + + String pathInContext=URIUtil.addPaths(servletPath,pathInfo); + + boolean endsWithSlash=(pathInfo==null?request.getServletPath():pathInfo).endsWith(URIUtil.SLASH); + boolean gzippable=_gzip && !endsWithSlash && !included && reqRanges==null; + + HttpContent content=null; + boolean release_content=true; + try + { + // Find the content + content=_contentFactory.getContent(pathInContext,response.getBufferSize()); + if (LOG.isDebugEnabled()) + LOG.info("content={}",content); + + // Not found? + if (content==null || !content.getResource().exists()) + { + if (included) + throw new FileNotFoundException("!" + pathInContext); + notFound(request,response); + return; + } + + // Directory? + if (content.getResource().isDirectory()) + { + sendWelcome(content,pathInContext,endsWithSlash,included,request,response); + return; + } + + // Strip slash? + 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(request.getContextPath(),pathInContext))); + return; + } + + // Conditional response? + if (!included && !passConditionalHeaders(request,response,content)) + return; + + // Gzip? + HttpContent gzip_content = gzippable?content.getGzipContent():null; + if (gzip_content!=null) + { + // Tell caches that response may vary by accept-encoding + response.addHeader(HttpHeader.VARY.asString(),HttpHeader.ACCEPT_ENCODING.asString()); + + // Does the client accept gzip? + String accept=request.getHeader(HttpHeader.ACCEPT_ENCODING.asString()); + if (accept!=null && accept.indexOf("gzip")>=0) + { + if (LOG.isDebugEnabled()) + LOG.debug("gzip={}",gzip_content); + content=gzip_content; + } + } + + // TODO this should be done by HttpContent#getContentEncoding + if (isGzippedContent(pathInContext)) + response.setHeader(HttpHeader.CONTENT_ENCODING.asString(),"gzip"); + + // Send the data + release_content=sendData(request,response,included,content,reqRanges); + + } + catch(IllegalArgumentException e) + { + LOG.warn(Log.EXCEPTION,e); + if(!response.isCommitted()) + response.sendError(500, e.getMessage()); + } + finally + { + if (release_content) + { + if (content!=null) + content.release(); + } + } + } + + + protected void sendWelcome(HttpContent content, String pathInContext, boolean endsWithSlash, boolean included, HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException + { + // Redirect to directory + 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())); + } + return; + } + + // look for a welcome file + String welcome=getWelcomeFile(pathInContext); + if (welcome!=null) + { + if (LOG.isDebugEnabled()) + LOG.debug("welcome={}",welcome); + if (_redirectWelcome) + { + // Redirect to the index + response.setContentLength(0); + String q=request.getQueryString(); + if (q!=null&&q.length()!=0) + response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(),welcome)+"?"+q)); + else + response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(),welcome))); + } + else + { + // 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); + } + } + } + return; + } + + if (included || passConditionalHeaders(request,response, content)) + sendDirectory(request,response,content.getResource(),pathInContext); + } + + /* ------------------------------------------------------------ */ + protected boolean isGzippedContent(String path) + { + if (path == null || _gzipEquivalentFileExtensions==null) + return false; + + for (String suffix:_gzipEquivalentFileExtensions) + if (path.endsWith(suffix)) + return true; + return false; + } + + /* ------------------------------------------------------------ */ + private boolean hasDefinedRange(Enumeration<String> reqRanges) + { + return (reqRanges!=null && reqRanges.hasMoreElements()); + } + + /* ------------------------------------------------------------ */ + /** + * Finds a matching welcome file for the supplied {@link Resource}. + * @param pathInContext the path of the request + * @return The path of the matching welcome file in context or null. + */ + protected abstract String getWelcomeFile(String pathInContext); + + protected abstract void notFound(HttpServletRequest request, HttpServletResponse response) throws IOException; + + /* ------------------------------------------------------------ */ + /* Check modification date headers. + */ + protected boolean passConditionalHeaders(HttpServletRequest request,HttpServletResponse response, HttpContent content) + throws IOException + { + try + { + String ifm=null; + String ifnm=null; + String ifms=null; + long ifums=-1; + + if (request instanceof Request) + { + // Find multiple fields by iteration as an optimization + HttpFields fields = ((Request)request).getHttpFields(); + for (int i=fields.size();i-->0;) + { + HttpField field=fields.getField(i); + if (field.getHeader() != null) + { + switch (field.getHeader()) + { + case IF_MATCH: + ifm=field.getValue(); + break; + case IF_NONE_MATCH: + ifnm=field.getValue(); + break; + case IF_MODIFIED_SINCE: + ifms=field.getValue(); + break; + case IF_UNMODIFIED_SINCE: + ifums=DateParser.parseDate(field.getValue()); + break; + default: + } + } + } + } + else + { + ifm=request.getHeader(HttpHeader.IF_MATCH.asString()); + ifnm=request.getHeader(HttpHeader.IF_NONE_MATCH.asString()); + ifms=request.getHeader(HttpHeader.IF_MODIFIED_SINCE.asString()); + ifums=request.getDateHeader(HttpHeader.IF_UNMODIFIED_SINCE.asString()); + } + + if (!HttpMethod.HEAD.is(request.getMethod())) + { + if (_etags) + { + String etag=content.getETagValue(); + if (ifm!=null) + { + boolean match=false; + if (etag!=null) + { + QuotedStringTokenizer quoted = new QuotedStringTokenizer(ifm,", ",false,true); + while (!match && quoted.hasMoreTokens()) + { + String tag = quoted.nextToken(); + if (etag.equals(tag) || tag.endsWith(ETAG_GZIP_QUOTE) && etag.equals(removeGzipFromETag(tag))) + match=true; + } + } + + if (!match) + { + response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED); + return false; + } + } + + if (ifnm!=null && etag!=null) + { + // Handle special case of exact match OR gzip exact match + if (etag.equals(ifnm) || ifnm.endsWith(ETAG_GZIP_QUOTE) && ifnm.indexOf(',')<0 && etag.equals(removeGzipFromETag(etag))) + { + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + response.setHeader(HttpHeader.ETAG.asString(),ifnm); + return false; + } + + // Handle list of tags + QuotedStringTokenizer quoted = new QuotedStringTokenizer(ifnm,", ",false,true); + while (quoted.hasMoreTokens()) + { + String tag = quoted.nextToken(); + if (etag.equals(tag) || tag.endsWith(ETAG_GZIP_QUOTE) && etag.equals(removeGzipFromETag(tag))) + { + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + response.setHeader(HttpHeader.ETAG.asString(),tag); + return false; + } + } + + // If etag requires content to be served, then do not check if-modified-since + return true; + } + } + + // Handle if modified since + if (ifms!=null) + { + //Get jetty's Response impl + String mdlm=content.getLastModifiedValue(); + if (mdlm!=null && ifms.equals(mdlm)) + { + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + if (_etags) + response.setHeader(HttpHeader.ETAG.asString(),content.getETagValue()); + response.flushBuffer(); + return false; + } + + long ifmsl=request.getDateHeader(HttpHeader.IF_MODIFIED_SINCE.asString()); + if (ifmsl!=-1 && content.getResource().lastModified()/1000 <= ifmsl/1000) + { + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + if (_etags) + response.setHeader(HttpHeader.ETAG.asString(),content.getETagValue()); + response.flushBuffer(); + return false; + } + } + + // Parse the if[un]modified dates and compare to resource + if (ifums!=-1 && content.getResource().lastModified()/1000 > ifums/1000) + { + response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED); + return false; + } + + } + } + catch(IllegalArgumentException iae) + { + if(!response.isCommitted()) + response.sendError(400, iae.getMessage()); + throw iae; + } + return true; + } + + + /* ------------------------------------------------------------------- */ + protected void sendDirectory(HttpServletRequest request, + HttpServletResponse response, + Resource resource, + String pathInContext) + throws IOException + { + if (!_dirAllowed) + { + response.sendError(HttpServletResponse.SC_FORBIDDEN); + return; + } + + byte[] data=null; + String base = URIUtil.addPaths(request.getRequestURI(),URIUtil.SLASH); + String dir = resource.getListHTML(base,pathInContext.length()>1); + if (dir==null) + { + response.sendError(HttpServletResponse.SC_FORBIDDEN, + "No directory"); + return; + } + + data=dir.getBytes("utf-8"); + response.setContentType("text/html;charset=utf-8"); + response.setContentLength(data.length); + response.getOutputStream().write(data); + } + + /* ------------------------------------------------------------ */ + protected boolean sendData(HttpServletRequest request, + HttpServletResponse response, + boolean include, + final HttpContent content, + Enumeration<String> reqRanges) + throws IOException + { + final long content_length = content.getContentLengthValue(); + + // Get the output stream (or writer) + OutputStream out =null; + boolean written; + try + { + out = response.getOutputStream(); + + // has something already written to the response? + written = out instanceof HttpOutput + ? ((HttpOutput)out).isWritten() + : true; + } + catch(IllegalStateException e) + { + out = new WriterOutputStream(response.getWriter()); + written=true; // there may be data in writer buffer, so assume written + } + + if (LOG.isDebugEnabled()) + LOG.debug(String.format("sendData content=%s out=%s async=%b",content,out,request.isAsyncSupported())); + + if ( reqRanges == null || !reqRanges.hasMoreElements() || content_length<0) + { + // if there were no ranges, send entire entity + if (include) + { + // write without headers + content.getResource().writeTo(out,0,content_length); + } + // else if we can't do a bypass write because of wrapping + else if (written || !(out instanceof HttpOutput)) + { + // write normally + putHeaders(response,content,written?-1:0); + ByteBuffer buffer = content.getIndirectBuffer(); + if (buffer!=null) + BufferUtil.writeTo(buffer,out); + else + content.getResource().writeTo(out,0,content_length); + } + // else do a bypass write + else + { + // write the headers + putHeaders(response,content,0); + + // write the content asynchronously if supported + if (request.isAsyncSupported() && content.getContentLengthValue()>response.getBufferSize()) + { + final AsyncContext context = request.startAsync(); + context.setTimeout(0); + + ((HttpOutput)out).sendContent(content,new Callback() + { + @Override + public void succeeded() + { + context.complete(); + content.release(); + } + + @Override + public void failed(Throwable x) + { + if (x instanceof IOException) + LOG.debug(x); + else + LOG.warn(x); + context.complete(); + content.release(); + } + + @Override + public String toString() + { + return String.format("ResourceService@%x$CB", ResourceService.this.hashCode()); + } + }); + return false; + } + // otherwise write content blocking + ((HttpOutput)out).sendContent(content); + } + } + else + { + // Parse the satisfiable ranges + List<InclusiveByteRange> ranges =InclusiveByteRange.satisfiableRanges(reqRanges,content_length); + + // if there are no satisfiable ranges, send 416 response + if (ranges==null || ranges.size()==0) + { + putHeaders(response,content,0); + response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); + response.setHeader(HttpHeader.CONTENT_RANGE.asString(), + InclusiveByteRange.to416HeaderRangeString(content_length)); + content.getResource().writeTo(out,0,content_length); + return true; + } + + // 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 = ranges.get(0); + long singleLength = singleSatisfiableRange.getSize(content_length); + putHeaders(response,content,singleLength); + response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + if (!response.containsHeader(HttpHeader.DATE.asString())) + response.addDateHeader(HttpHeader.DATE.asString(),System.currentTimeMillis()); + response.setHeader(HttpHeader.CONTENT_RANGE.asString(), + singleSatisfiableRange.toHeaderRangeString(content_length)); + content.getResource().writeTo(out,singleSatisfiableRange.getFirst(content_length),singleLength); + return true; + } + + // multiple non-overlapping valid ranges cause a multipart + // 216 response which does not require an overall + // content-length header + // + putHeaders(response,content,-1); + String mimetype=(content==null?null:content.getContentTypeValue()); + if (mimetype==null) + LOG.warn("Unknown mimetype for "+request.getRequestURI()); + MultiPartOutputStream multi = new MultiPartOutputStream(out); + response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + if (!response.containsHeader(HttpHeader.DATE.asString())) + response.addDateHeader(HttpHeader.DATE.asString(),System.currentTimeMillis()); + + // 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(HttpHeader.REQUEST_RANGE.asString())!=null) + ctp = "multipart/x-byteranges; boundary="; + else + ctp = "multipart/byteranges; boundary="; + response.setContentType(ctp+multi.getBoundary()); + + InputStream in=content.getResource().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 = ranges.get(i); + header[i]=ibr.toHeaderRangeString(content_length); + length+= + ((i>0)?2:0)+ + 2+multi.getBoundary().length()+2+ + (mimetype==null?0:HttpHeader.CONTENT_TYPE.asString().length()+2+mimetype.length())+2+ + HttpHeader.CONTENT_RANGE.asString().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 = ranges.get(i); + multi.startPart(mimetype,new String[]{HttpHeader.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=content.getResource().getInputStream(); + pos=0; + } + if (pos<start) + { + in.skip(start-pos); + pos=start; + } + + IO.copy(in,multi,size); + pos+=size; + } + else + // Handle cached resource + content.getResource().writeTo(multi,start,size); + } + if (in!=null) + in.close(); + multi.close(); + } + return true; + } + + /* ------------------------------------------------------------ */ + protected void putHeaders(HttpServletResponse response,HttpContent content, long contentLength) + { + if (response instanceof Response) + { + Response r = (Response)response; + r.putHeaders(content,contentLength,_etags); + HttpFields f = r.getHttpFields(); + if (_acceptRanges) + f.put(ACCEPT_RANGES); + + if (_cacheControl!=null) + f.put(_cacheControl); + } + else + { + Response.putHeaders(response,content,contentLength,_etags); + if (_acceptRanges) + response.setHeader(ACCEPT_RANGES.getName(),ACCEPT_RANGES.getValue()); + + if (_cacheControl!=null) + response.setHeader(_cacheControl.getName(),_cacheControl.getValue()); + } + } + +} |