diff options
21 files changed, 1300 insertions, 181 deletions
diff --git a/VERSION.txt b/VERSION.txt index 0f5200874b..57a9b1beaf 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -217,6 +217,11 @@ jetty-9.3.0.v20150612 - 12 June 2015 --add-to-start + 469991 Fix logging levels in websocket client UpgradeConnection +jetty-9.2.12.v20150709 - 09 July 2015 + + 469414 Proxied redirects expose upstream server name. + + 469936 Remove usages of SpinLock. + + 470184 Send the proxy-to-server request more lazily. + jetty-9.2.11.v20150529 - 29 May 2015 + 461499 ConnectionPool may leak connections. + 463579 Add support for 308 status code. diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/PathMap.java b/jetty-http/src/main/java/org/eclipse/jetty/http/PathMap.java index 84eb956c08..bb30c3b4d1 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/PathMap.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/PathMap.java @@ -18,14 +18,18 @@ package org.eclipse.jetty.http; +import java.util.AbstractSet; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.StringTokenizer; +import java.util.function.BiFunction; import org.eclipse.jetty.util.ArrayTernaryTrie; +import org.eclipse.jetty.util.RegexSet; import org.eclipse.jetty.util.Trie; import org.eclipse.jetty.util.URIUtil; @@ -587,4 +591,48 @@ public class PathMap<O> extends HashMap<String,O> this.mapped = mapped; } } + + public static class PathSet extends AbstractSet<String> + { + public static final BiFunction<PathSet,String,Boolean> MATCHER=(s,e)->{return s.containsMatch(e);}; + private final PathMap<Boolean> _map = new PathMap<>(); + + @Override + public Iterator<String> iterator() + { + return _map.keySet().iterator(); + } + + @Override + public int size() + { + return _map.size(); + } + + @Override + public boolean add(String item) + { + return _map.put(item,Boolean.TRUE)==null; + } + + @Override + public boolean remove(Object item) + { + return _map.remove(item)!=null; + } + + @Override + public boolean contains(Object o) + { + return _map.containsKey(o); + } + + + public boolean containsMatch(String s) + { + return _map.containsMatch(s); + } + + + } } diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractConnection.java b/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractConnection.java index 2cf0357f06..52f7b4c83f 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractConnection.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractConnection.java @@ -92,7 +92,14 @@ public abstract class AbstractConnection implements Connection @Override public void run() { - callback.failed(x); + try + { + callback.failed(x); + } + catch(Exception e) + { + LOG.warn(e); + } } }); } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java index fcc6a8a83b..db91af9ba5 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java @@ -56,6 +56,7 @@ public class HttpConfiguration private int _responseHeaderSize=8*1024; private int _headerCacheSize=512; private int _securePort; + private long _blockingTimeout=-1; private String _secureScheme = HttpScheme.HTTPS.asString(); private boolean _sendServerVersion = true; private boolean _sendXPoweredBy = false; @@ -102,15 +103,21 @@ public class HttpConfiguration public HttpConfiguration(HttpConfiguration config) { _customizers.addAll(config._customizers); + for (String s:config._formEncodedMethods.keySet()) + _formEncodedMethods.put(s,Boolean.TRUE); _outputBufferSize=config._outputBufferSize; _outputAggregationSize=config._outputAggregationSize; _requestHeaderSize=config._requestHeaderSize; _responseHeaderSize=config._responseHeaderSize; - _securePort=config._securePort; + _headerCacheSize=config._headerCacheSize; _secureScheme=config._secureScheme; + _securePort=config._securePort; + _blockingTimeout=config._blockingTimeout; _sendDateHeader=config._sendDateHeader; _sendServerVersion=config._sendServerVersion; - _headerCacheSize=config._headerCacheSize; + _sendXPoweredBy=config._sendXPoweredBy; + _delayDispatchUntilContent=config._delayDispatchUntilContent; + _persistentConnectionsEnabled=config._persistentConnectionsEnabled; } /* ------------------------------------------------------------ */ @@ -198,6 +205,33 @@ public class HttpConfiguration } /* ------------------------------------------------------------ */ + /** Get the timeout applied to blocking operations. + * <p>This timeout is in addition to the {@link Connector#getIdleTimeout()}, and applies + * to the total operation (as opposed to the idle timeout that applies to the time no + * data is being sent). + * @return -1, for no blocking timeout (default), 0 for a blocking timeout equal to the + * idle timeout; >0 for a timeout in ms applied to the total blocking operation. + */ + @ManagedAttribute("Timeout in MS for blocking operations.") + public long getBlockingTimeout() + { + return _blockingTimeout; + } + + /** + * Set the timeout applied to blocking operations. + * <p>This timeout is in addition to the {@link Connector#getIdleTimeout()}, and applies + * to the total operation (as opposed to the idle timeout that applies to the time no + * data is being sent). + * @param blockingTimeout -1, for no blocking timeout (default), 0 for a blocking timeout equal to the + * idle timeout; >0 for a timeout in ms applied to the total blocking operation. + */ + public void setBlockingTimeout(long blockingTimeout) + { + _blockingTimeout = blockingTimeout; + } + + /* ------------------------------------------------------------ */ public void setPersistentConnectionsEnabled(boolean persistentConnectionsEnabled) { _persistentConnectionsEnabled = persistentConnectionsEnabled; diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java index b9f79a292a..9ab2da7e11 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java @@ -783,6 +783,11 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http { getEndPoint().fillInterested(_blockingReadCallback); } + + public void blockingReadException(Throwable e) + { + _blockingReadCallback.failed(e); + } @Override public String toString() diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java index 031bb10c0f..564e1eebc3 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.InterruptedIOException; import java.nio.ByteBuffer; import java.util.Objects; +import java.util.concurrent.TimeoutException; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; @@ -54,10 +55,13 @@ public class HttpInput extends ServletInputStream implements Runnable private ReadListener _listener; private State _state = STREAM; private long _contentConsumed; + private long _blockingTimeoutAt = -1; public HttpInput(HttpChannelState state) { _channelState=state; + if (_channelState.getHttpChannel().getHttpConfiguration().getBlockingTimeout()>0) + _blockingTimeoutAt=0; } protected HttpChannelState getHttpChannelState() @@ -131,6 +135,9 @@ public class HttpInput extends ServletInputStream implements Runnable { synchronized (_inputQ) { + if (_blockingTimeoutAt>=0 && !isAsync()) + _blockingTimeoutAt=System.currentTimeMillis()+getHttpChannelState().getHttpChannel().getHttpConfiguration().getBlockingTimeout(); + while(true) { Content item = nextContent(); @@ -337,11 +344,25 @@ public class HttpInput extends ServletInputStream implements Runnable { try { + long timeout=0; + if (_blockingTimeoutAt>=0) + { + timeout=_blockingTimeoutAt-System.currentTimeMillis(); + if (timeout<=0) + throw new TimeoutException(); + } + if (LOG.isDebugEnabled()) - LOG.debug("{} blocking for content...", this); - _inputQ.wait(); + LOG.debug("{} blocking for content timeout={} ...", this,timeout); + if (timeout>0) + _inputQ.wait(timeout); + else + _inputQ.wait(); + + if (_blockingTimeoutAt>0 && System.currentTimeMillis()>=_blockingTimeoutAt) + throw new TimeoutException(); } - catch (InterruptedException e) + catch (Throwable e) { throw (IOException)new InterruptedIOException().initCause(e); } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInputOverHTTP.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInputOverHTTP.java index df8fbd5b53..f8f718f0d9 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInputOverHTTP.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInputOverHTTP.java @@ -37,6 +37,13 @@ public class HttpInputOverHTTP extends HttpInput protected void blockForContent() throws IOException { ((HttpConnection)getHttpChannelState().getHttpChannel().getEndPoint().getConnection()).blockingReadFillInterested(); - super.blockForContent(); + try + { + super.blockForContent(); + } + catch(Throwable e) + { + ((HttpConnection)getHttpChannelState().getHttpChannel().getEndPoint().getConnection()).blockingReadException(e); + } } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java index 940ea3f7fc..c16257bb36 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java @@ -98,6 +98,11 @@ public class HttpOutput extends ServletOutputStream implements Runnable @Override protected long getIdleTimeout() { + long bto = getHttpChannel().getHttpConfiguration().getBlockingTimeout(); + if (bto>0) + return bto; + if (bto<0) + return -1; return _channel.getIdleTimeout(); } }; @@ -585,6 +590,9 @@ public class HttpOutput extends ServletOutputStream implements Runnable */ public void sendContent(ByteBuffer content) throws IOException { + if (LOG.isDebugEnabled()) + LOG.debug("sendContent({})",BufferUtil.toDetailString(content)); + write(content, true); closed(); } @@ -662,6 +670,9 @@ public class HttpOutput extends ServletOutputStream implements Runnable */ public void sendContent(ByteBuffer content, final Callback callback) { + if (LOG.isDebugEnabled()) + LOG.debug("sendContent(buffer={},{})",BufferUtil.toDetailString(content),callback); + write(content, true, new Callback() { @Override @@ -689,6 +700,9 @@ public class HttpOutput extends ServletOutputStream implements Runnable */ public void sendContent(InputStream in, Callback callback) { + if (LOG.isDebugEnabled()) + LOG.debug("sendContent(stream={},{})",in,callback); + new InputStreamWritingCB(in, callback).iterate(); } @@ -701,6 +715,9 @@ public class HttpOutput extends ServletOutputStream implements Runnable */ public void sendContent(ReadableByteChannel in, Callback callback) { + if (LOG.isDebugEnabled()) + LOG.debug("sendContent(channel={},{})",in,callback); + new ReadableByteChannelWritingCB(in, callback).iterate(); } @@ -712,6 +729,9 @@ public class HttpOutput extends ServletOutputStream implements Runnable */ public void sendContent(HttpContent httpContent, Callback callback) { + if (LOG.isDebugEnabled()) + LOG.debug("sendContent(http={},{})",httpContent,callback); + if (BufferUtil.hasContent(_aggregate)) { callback.failed(new IOException("cannot sendContent() after write()")); @@ -752,9 +772,6 @@ public class HttpOutput extends ServletOutputStream implements Runnable if (buffer!=null) { - if (LOG.isDebugEnabled()) - LOG.debug("sendContent({}=={},{},direct={})",httpContent,BufferUtil.toDetailString(buffer),callback,_channel.useDirectBuffers()); - sendContent(buffer,callback); return; } @@ -764,8 +781,6 @@ public class HttpOutput extends ServletOutputStream implements Runnable ReadableByteChannel rbc=httpContent.getReadableByteChannel(); if (rbc!=null) { - if (LOG.isDebugEnabled()) - LOG.debug("sendContent({}=={},{},direct={})",httpContent,rbc,callback,_channel.useDirectBuffers()); // Close of the rbc is done by the async sendContent sendContent(rbc,callback); return; @@ -774,8 +789,6 @@ public class HttpOutput extends ServletOutputStream implements Runnable InputStream in = httpContent.getInputStream(); if (in!=null) { - if (LOG.isDebugEnabled()) - LOG.debug("sendContent({}=={},{},direct={})",httpContent,in,callback,_channel.useDirectBuffers()); sendContent(in,callback); return; } @@ -1102,6 +1115,8 @@ public class HttpOutput extends ServletOutputStream implements Runnable return Action.SCHEDULED; } + if (LOG.isDebugEnabled() && _completed) + LOG.debug("EOF of {}",this); return Action.SUCCEEDED; } @@ -1142,6 +1157,8 @@ public class HttpOutput extends ServletOutputStream implements Runnable // a write done with EOF=true if (_eof) { + if (LOG.isDebugEnabled()) + LOG.debug("EOF of {}",this); // Handle EOF _in.close(); closed(); @@ -1206,6 +1223,8 @@ public class HttpOutput extends ServletOutputStream implements Runnable // a write done with EOF=true if (_eof) { + if (LOG.isDebugEnabled()) + LOG.debug("EOF of {}",this); _in.close(); closed(); _channel.getByteBufferPool().release(_buffer); @@ -1220,7 +1239,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable // write what we have _buffer.flip(); write(_buffer,_eof,this); - + return Action.SCHEDULED; } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java index e2aac872db..0ce72da45b 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java @@ -20,10 +20,7 @@ package org.eclipse.jetty.server.handler.gzip; import java.io.File; import java.io.IOException; -import java.util.Arrays; -import java.util.HashSet; import java.util.Set; -import java.util.regex.Pattern; import java.util.zip.Deflater; import javax.servlet.ServletContext; @@ -40,7 +37,8 @@ import org.eclipse.jetty.http.PathMap; import org.eclipse.jetty.server.HttpOutput; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.handler.HandlerWrapper; -import org.eclipse.jetty.util.ConcurrentHashSet; +import org.eclipse.jetty.util.IncludeExclude; +import org.eclipse.jetty.util.RegexSet; import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -73,16 +71,12 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory // non-static, as other GzipHandler instances may have different configurations private final ThreadLocal<Deflater> _deflater = new ThreadLocal<Deflater>(); - private final Set<String> _includedMethods=new HashSet<>(); - private final Set<Pattern> _excludedAgentPatterns=new HashSet<>(); - private final PathMap<Boolean> _excludedPaths=new PathMap<>(); - private final PathMap<Boolean> _includedPaths=new PathMap<>(); - private final Set<String> _excludedMimeTypes=new HashSet<>(); - private final Set<String> _includedMimeTypes=new HashSet<>(); - private HttpField _vary; + private final IncludeExclude<String> _agentPatterns=new IncludeExclude<>(RegexSet.class,RegexSet.MATCHER); + private final IncludeExclude<String> _methods = new IncludeExclude<>(); + private final IncludeExclude<String> _paths = new IncludeExclude<>(PathMap.PathSet.class,PathMap.PathSet.MATCHER); + private final IncludeExclude<String> _mimeTypes = new IncludeExclude<>(); - private final Set<String> _uaCache = new ConcurrentHashSet<>(); - private int _uaCacheSize = 1024; + private HttpField _vary; /* ------------------------------------------------------------ */ /** @@ -94,22 +88,24 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory */ public GzipHandler() { - _includedMethods.add(HttpMethod.GET.asString()); + _methods.include(HttpMethod.GET.asString()); for (String type:MimeTypes.getKnownMimeTypes()) { - if (type.startsWith("image/")|| + if ("image/svg+xml".equals(type)) + _paths.exclude("*.svgz"); + else if (type.startsWith("image/")|| type.startsWith("audio/")|| type.startsWith("video/")) - _excludedMimeTypes.add(type); + _mimeTypes.exclude(type); } - _excludedMimeTypes.add("application/compress"); - _excludedMimeTypes.add("application/zip"); - _excludedMimeTypes.add("application/gzip"); - _excludedMimeTypes.add("application/bzip2"); - _excludedMimeTypes.add("application/x-rar-compressed"); - LOG.debug("{} excluding mimes {}",this,_excludedMimeTypes); + _mimeTypes.exclude("application/compress"); + _mimeTypes.exclude("application/zip"); + _mimeTypes.exclude("application/gzip"); + _mimeTypes.exclude("application/bzip2"); + _mimeTypes.exclude("application/x-rar-compressed"); + LOG.debug("{} mime types {}",this,_mimeTypes); - _excludedAgentPatterns.add(Pattern.compile(".*MSIE 6.0.*")); + _agentPatterns.exclude(".*MSIE 6.0.*"); } /* ------------------------------------------------------------ */ @@ -118,8 +114,17 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory */ public void addExcludedAgentPatterns(String... patterns) { - for (String s : patterns) - _excludedAgentPatterns.add(Pattern.compile(s)); + _agentPatterns.exclude(patterns); + } + + /* ------------------------------------------------------------ */ + /** + * @param methods The methods to exclude in compression + */ + public void addExcludedMethods(String... methods) + { + for (String m : methods) + _methods.exclude(m); } /* ------------------------------------------------------------ */ @@ -129,7 +134,7 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory */ public void addExcludedMimeTypes(String... types) { - _excludedMimeTypes.addAll(Arrays.asList(types)); + _mimeTypes.exclude(types); } /* ------------------------------------------------------------ */ @@ -140,18 +145,26 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory */ public void addExcludedPaths(String... pathspecs) { - for (String ps : pathspecs) - _excludedPaths.put(ps,Boolean.TRUE); + _paths.exclude(pathspecs); } /* ------------------------------------------------------------ */ /** + * @param patterns Regular expressions matching user agents to exclude + */ + public void addIncludedAgentPatterns(String... patterns) + { + _agentPatterns.include(patterns); + } + + /* ------------------------------------------------------------ */ + /** * @param methods The methods to include in compression */ public void addIncludedMethods(String... methods) { for (String m : methods) - _includedMethods.add(m); + _methods.include(m); } /* ------------------------------------------------------------ */ @@ -162,7 +175,7 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory */ public void addIncludedMimeTypes(String... types) { - _includedMimeTypes.addAll(Arrays.asList(types)); + _mimeTypes.include(types); } /* ------------------------------------------------------------ */ @@ -174,11 +187,18 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory */ public void addIncludedPaths(String... pathspecs) { - for (String ps : pathspecs) - _includedPaths.put(ps,Boolean.TRUE); + _paths.include(pathspecs); } /* ------------------------------------------------------------ */ + @Override + protected void doStart() throws Exception + { + _vary=(_agentPatterns.size()>0)?GzipHttpOutputInterceptor.VARY_ACCEPT_ENCODING_USER_AGENT:GzipHttpOutputInterceptor.VARY_ACCEPT_ENCODING; + super.doStart(); + } + + /* ------------------------------------------------------------ */ public boolean getCheckGzExists() { return _checkGzExists; @@ -189,13 +209,13 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory { return _compressionLevel; } - + /* ------------------------------------------------------------ */ @Override public Deflater getDeflater(Request request, long content_length) { String ua = request.getHttpFields().get(HttpHeader.USER_AGENT); - if (ua!=null && isExcludedAgent(ua)) + if (ua!=null && !isAgentGzipable(ua)) { LOG.debug("{} excluded user agent {}",this,request); return null; @@ -238,45 +258,65 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory /* ------------------------------------------------------------ */ public String[] getExcludedAgentPatterns() { - Pattern[] ps = _excludedAgentPatterns.toArray(new Pattern[_excludedAgentPatterns.size()]); - String[] s = new String[ps.length]; - - int i=0; - for (Pattern p: ps) - s[i++]=p.toString(); - return s; + Set<String> excluded=_agentPatterns.getExcluded(); + return excluded.toArray(new String[excluded.size()]); } - + + /* ------------------------------------------------------------ */ + public String[] getExcludedMethods() + { + Set<String> excluded=_methods.getExcluded(); + return excluded.toArray(new String[excluded.size()]); + } + /* ------------------------------------------------------------ */ public String[] getExcludedMimeTypes() { - return _excludedMimeTypes.toArray(new String[_excludedMimeTypes.size()]); + Set<String> excluded=_mimeTypes.getExcluded(); + return excluded.toArray(new String[excluded.size()]); } /* ------------------------------------------------------------ */ public String[] getExcludedPaths() { - String[] ps = _excludedPaths.keySet().toArray(new String[_excludedPaths.size()]); - return ps; + Set<String> excluded=_paths.getExcluded(); + return excluded.toArray(new String[excluded.size()]); + } + + + /* ------------------------------------------------------------ */ + public String[] getIncludedAgentPatterns() + { + Set<String> includes=_agentPatterns.getIncluded(); + return includes.toArray(new String[includes.size()]); + } + + /* ------------------------------------------------------------ */ + public String[] getIncludedMethods() + { + Set<String> includes=_methods.getIncluded(); + return includes.toArray(new String[includes.size()]); } /* ------------------------------------------------------------ */ public String[] getIncludedMimeTypes() { - return _includedMimeTypes.toArray(new String[_includedMimeTypes.size()]); + Set<String> includes=_mimeTypes.getIncluded(); + return includes.toArray(new String[includes.size()]); } /* ------------------------------------------------------------ */ public String[] getIncludedPaths() { - String[] ps = _includedPaths.keySet().toArray(new String[_includedPaths.size()]); - return ps; + Set<String> includes=_paths.getIncluded(); + return includes.toArray(new String[includes.size()]); } /* ------------------------------------------------------------ */ + @Deprecated public String[] getMethods() { - return _includedMethods.toArray(new String[_includedMethods.size()]); + return getIncludedMethods(); } /* ------------------------------------------------------------ */ @@ -291,14 +331,6 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory } /* ------------------------------------------------------------ */ - @Override - protected void doStart() throws Exception - { - _vary=(_excludedAgentPatterns.size()>0)?GzipHttpOutputInterceptor.VARY_ACCEPT_ENCODING_USER_AGENT:GzipHttpOutputInterceptor.VARY_ACCEPT_ENCODING; - super.doStart(); - } - - /* ------------------------------------------------------------ */ /** * @see org.eclipse.jetty.server.handler.HandlerWrapper#handle(java.lang.String, org.eclipse.jetty.server.Request, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) */ @@ -324,7 +356,7 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory } // If not a supported method - no Vary because no matter what client, this URI is always excluded - if (!_includedMethods.contains(baseRequest.getMethod())) + if (!_methods.matches(baseRequest.getMethod())) { LOG.debug("{} excluded by method {}",this,request); _handler.handle(target,baseRequest, request, response); @@ -392,39 +424,19 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory * @param ua the user agent * @return boolean true if excluded */ - protected boolean isExcludedAgent(String ua) + protected boolean isAgentGzipable(String ua) { if (ua == null) return false; - if (_excludedAgentPatterns != null) - { - - if (_uaCache.contains(ua)) - return true; - - for (Pattern pattern : _excludedAgentPatterns) - { - if (pattern.matcher(ua).matches()) - { - if (_uaCache.size()>_uaCacheSize) - _uaCache.clear(); - _uaCache.add(ua); - return true; - } - } - } - - return false; + return _agentPatterns.matches(ua); } /* ------------------------------------------------------------ */ @Override public boolean isMimeTypeGzipable(String mimetype) { - if (_includedMimeTypes.size()>0 && _includedMimeTypes.contains(mimetype)) - return true; - return !_excludedMimeTypes.contains(mimetype); + return _mimeTypes.matches(mimetype); } /* ------------------------------------------------------------ */ @@ -440,13 +452,7 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory if (requestURI == null) return true; - if (_includedPaths.size()>0 && _includedPaths.containsMatch(requestURI)) - return true; - - if (_excludedPaths.size()>0 && _excludedPaths.containsMatch(requestURI)) - return false; - - return true; + return _paths.matches(requestURI); } /* ------------------------------------------------------------ */ @@ -484,19 +490,29 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory */ public void setExcludedAgentPatterns(String... patterns) { - _excludedAgentPatterns.clear(); + _agentPatterns.getExcluded().clear(); addExcludedAgentPatterns(patterns); } /* ------------------------------------------------------------ */ /** + * @param method to exclude + */ + public void setExcludedMethods(String... method) + { + _methods.getExcluded().clear(); + _methods.exclude(method); + } + + /* ------------------------------------------------------------ */ + /** * Set the mime types. * @param types The mime types to exclude (without charset or other parameters) */ public void setExcludedMimeTypes(String... types) { - _excludedMimeTypes.clear(); - addExcludedMimeTypes(types); + _mimeTypes.getExcluded().clear(); + _mimeTypes.exclude(types); } /* ------------------------------------------------------------ */ @@ -507,18 +523,28 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory */ public void setExcludedPaths(String... pathspecs) { - _excludedPaths.clear(); - addExcludedPaths(pathspecs); + _paths.getExcluded().clear(); + _paths.exclude(pathspecs); } /* ------------------------------------------------------------ */ /** + * @param patterns Regular expressions matching user agents to include + */ + public void setIncludedAgentPatterns(String... patterns) + { + _agentPatterns.getIncluded().clear(); + addIncludedAgentPatterns(patterns); + } + + /* ------------------------------------------------------------ */ + /** * @param methods The methods to include in compression */ public void setIncludedMethods(String... methods) { - _includedMethods.clear(); - addIncludedMethods(methods); + _methods.getIncluded().clear(); + _methods.include(methods); } /* ------------------------------------------------------------ */ @@ -529,8 +555,8 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory */ public void setIncludedMimeTypes(String... types) { - _includedMimeTypes.clear(); - addIncludedMimeTypes(types); + _mimeTypes.getIncluded().clear(); + _mimeTypes.include(types); } /* ------------------------------------------------------------ */ @@ -542,8 +568,8 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory */ public void setIncludedPaths(String... pathspecs) { - _includedPaths.clear(); - addIncludedPaths(pathspecs); + _paths.getIncluded().clear(); + _paths.include(pathspecs); } /* ------------------------------------------------------------ */ diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectorTimeoutTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectorTimeoutTest.java index a5646ae59a..5edff8a143 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectorTimeoutTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectorTimeoutTest.java @@ -18,11 +18,17 @@ package org.eclipse.jetty.server; +import java.io.BufferedInputStream; +import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.io.OutputStream; import java.net.Socket; import java.net.SocketException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.concurrent.Exchanger; import java.util.concurrent.TimeUnit; @@ -31,14 +37,18 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.io.AbstractConnection; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.ssl.SslConnection; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.toolchain.test.AdvancedRunner; import org.eclipse.jetty.toolchain.test.TestTracker; import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.StdErrLog; import org.hamcrest.Matchers; import org.junit.Assert; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -59,6 +69,13 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture System.setProperty("org.eclipse.jetty.io.nio.IDLE_TICK","100"); } + @Before + public void before() + { + super.before(); + if (_httpConfiguration!=null) + _httpConfiguration.setBlockingTimeout(-1L); + } @Test(timeout=60000) public void testMaxIdleWithRequest10() throws Exception @@ -351,6 +368,227 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture } @Test(timeout=60000) + public void testNoBlockingTimeoutRead() throws Exception + { + _httpConfiguration.setBlockingTimeout(-1L); + + configureServer(new EchoHandler()); + Socket client=newSocket(_serverURI.getHost(),_serverURI.getPort()); + client.setSoTimeout(10000); + InputStream is=client.getInputStream(); + Assert.assertFalse(client.isClosed()); + + OutputStream os=client.getOutputStream(); + os.write(("GET / HTTP/1.1\r\n"+ + "host: "+_serverURI.getHost()+":"+_serverURI.getPort()+"\r\n"+ + "Transfer-Encoding: chunked\r\n" + + "Content-Type: text/plain\r\n" + + "Connection: close\r\n" + + "\r\n"+ + "5\r\n"+ + "LMNOP\r\n") + .getBytes("utf-8")); + os.flush(); + + long start = System.currentTimeMillis(); + try + { + Thread.sleep(250); + os.write("1".getBytes("utf-8")); + os.flush(); + Thread.sleep(250); + os.write("0".getBytes("utf-8")); + os.flush(); + Thread.sleep(250); + os.write("\r".getBytes("utf-8")); + os.flush(); + Thread.sleep(250); + os.write("\n".getBytes("utf-8")); + os.flush(); + Thread.sleep(250); + os.write("0123456789ABCDEF\r\n".getBytes("utf-8")); + os.write("0\r\n".getBytes("utf-8")); + os.write("\r\n".getBytes("utf-8")); + os.flush(); + } + catch(Exception e) + { + e.printStackTrace(); + } + long duration=System.currentTimeMillis() - start; + Assert.assertThat(duration,Matchers.greaterThan(500L)); + + // read the response + String response = IO.toString(is); + Assert.assertThat(response,Matchers.startsWith("HTTP/1.1 200 OK")); + Assert.assertThat(response,Matchers.containsString("LMNOP0123456789ABCDEF")); + + } + + @Test(timeout=60000) + public void testBlockingTimeoutRead() throws Exception + { + _httpConfiguration.setBlockingTimeout(750L); + + configureServer(new EchoHandler()); + Socket client=newSocket(_serverURI.getHost(),_serverURI.getPort()); + client.setSoTimeout(10000); + InputStream is=client.getInputStream(); + Assert.assertFalse(client.isClosed()); + + OutputStream os=client.getOutputStream(); + os.write(("GET / HTTP/1.1\r\n"+ + "host: "+_serverURI.getHost()+":"+_serverURI.getPort()+"\r\n"+ + "Transfer-Encoding: chunked\r\n" + + "Content-Type: text/plain\r\n" + + "Connection: close\r\n" + + "\r\n"+ + "5\r\n"+ + "LMNOP\r\n") + .getBytes("utf-8")); + os.flush(); + + long start = System.currentTimeMillis(); + try + { + ((StdErrLog)Log.getLogger(HttpChannel.class)).setHideStacks(true); + Thread.sleep(300); + os.write("1".getBytes("utf-8")); + os.flush(); + Thread.sleep(300); + os.write("0".getBytes("utf-8")); + os.flush(); + Thread.sleep(300); + os.write("\r".getBytes("utf-8")); + os.flush(); + Thread.sleep(300); + os.write("\n".getBytes("utf-8")); + os.flush(); + Thread.sleep(300); + os.write("0123456789ABCDEF\r\n".getBytes("utf-8")); + os.write("0\r\n".getBytes("utf-8")); + os.write("\r\n".getBytes("utf-8")); + os.flush(); + } + catch(Exception e) + { + } + finally + { + ((StdErrLog)Log.getLogger(HttpChannel.class)).setHideStacks(false); + } + long duration=System.currentTimeMillis() - start; + Assert.assertThat(duration,Matchers.greaterThan(500L)); + + try + { + // read the response + String response = IO.toString(is); + Assert.assertThat(response,Matchers.startsWith("HTTP/1.1 500 ")); + Assert.assertThat(response,Matchers.containsString("InterruptedIOException")); + } + catch(SSLException e) + { + } + + } + + @Test(timeout=60000) + public void testNoBlockingTimeoutWrite() throws Exception + { + configureServer(new HugeResponseHandler()); + Socket client=newSocket(_serverURI.getHost(),_serverURI.getPort()); + client.setSoTimeout(10000); + + Assert.assertFalse(client.isClosed()); + + OutputStream os=client.getOutputStream(); + BufferedReader is=new BufferedReader(new InputStreamReader(client.getInputStream(),StandardCharsets.ISO_8859_1),2048); + + os.write(( + "GET / HTTP/1.0\r\n"+ + "host: "+_serverURI.getHost()+":"+_serverURI.getPort()+"\r\n"+ + "connection: keep-alive\r\n"+ + "Connection: close\r\n"+ + "\r\n").getBytes("utf-8")); + os.flush(); + + // read the header + String line=is.readLine(); + Assert.assertThat(line,Matchers.startsWith("HTTP/1.1 200 OK")); + while(line.length()!=0) + line=is.readLine(); + + for (int i=0;i<(128*1024);i++) + { + if (i%1028==0) + { + Thread.sleep(20); + // System.err.println("read "+System.currentTimeMillis()); + } + line=is.readLine(); + Assert.assertThat(line,Matchers.notNullValue()); + Assert.assertEquals(1022,line.length()); + } + } + + @Test(timeout=60000) + public void testBlockingTimeoutWrite() throws Exception + { + _httpConfiguration.setBlockingTimeout(750L); + configureServer(new HugeResponseHandler()); + Socket client=newSocket(_serverURI.getHost(),_serverURI.getPort()); + client.setSoTimeout(10000); + + Assert.assertFalse(client.isClosed()); + + OutputStream os=client.getOutputStream(); + BufferedReader is=new BufferedReader(new InputStreamReader(client.getInputStream(),StandardCharsets.ISO_8859_1),2048); + + os.write(( + "GET / HTTP/1.0\r\n"+ + "host: "+_serverURI.getHost()+":"+_serverURI.getPort()+"\r\n"+ + "connection: keep-alive\r\n"+ + "Connection: close\r\n"+ + "\r\n").getBytes("utf-8")); + os.flush(); + + // read the header + String line=is.readLine(); + Assert.assertThat(line,Matchers.startsWith("HTTP/1.1 200 OK")); + while(line.length()!=0) + line=is.readLine(); + + long start=System.currentTimeMillis(); + try + { + ((StdErrLog)Log.getLogger(HttpChannel.class)).setHideStacks(true); + ((StdErrLog)Log.getLogger(AbstractConnection.class)).setHideStacks(true); + for (int i=0;i<(128*1024);i++) + { + if (i%1028==0) + { + Thread.sleep(20); + // System.err.println("read "+System.currentTimeMillis()); + } + line=is.readLine(); + if (line==null) + break; + } + } + catch(Throwable e) + {} + finally + { + ((StdErrLog)Log.getLogger(AbstractConnection.class)).setHideStacks(false); + ((StdErrLog)Log.getLogger(HttpChannel.class)).setHideStacks(false); + } + long end=System.currentTimeMillis(); + long duration = end-start; + Assert.assertThat(duration,Matchers.lessThan(20L*128L)); + } + + @Test(timeout=60000) public void testMaxIdleNoRequest() throws Exception { configureServer(new EchoHandler()); @@ -522,6 +760,27 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture out.close(); } } + + protected static class HugeResponseHandler extends AbstractHandler + { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + baseRequest.setHandled(true); + response.setStatus(200); + OutputStream out = response.getOutputStream(); + byte[] buffer = new byte[128*1024*1024]; + Arrays.fill(buffer,(byte)'x'); + for (int i=0;i<128*1024;i++) + { + buffer[i*1024+1022]='\r'; + buffer[i*1024+1023]='\n'; + } + ByteBuffer bb = ByteBuffer.wrap(buffer); + ((HttpOutput)out).sendContent(bb); + out.close(); + } + } protected static class WaitHandler extends AbstractHandler { diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestFixture.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestFixture.java index 12a0c3a1ca..06def0cc90 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestFixture.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestFixture.java @@ -49,6 +49,7 @@ public class HttpServerTestFixture protected QueuedThreadPool _threadPool; protected Server _server; protected URI _serverURI; + protected HttpConfiguration _httpConfiguration; protected ServerConnector _connector; protected String _scheme="http"; @@ -76,7 +77,9 @@ public class HttpServerTestFixture protected void startServer(ServerConnector connector, Handler handler) throws Exception { _connector = connector; - _connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setSendDateHeader(false); + _httpConfiguration=_connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration(); + _httpConfiguration.setBlockingTimeout(-1); + _httpConfiguration.setSendDateHeader(false); _server.addConnector(_connector); _server.setHandler(handler); _server.start(); diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ConcatServlet.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ConcatServlet.java index 76ac313b2a..5207f857b8 100644 --- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ConcatServlet.java +++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ConcatServlet.java @@ -19,6 +19,8 @@ package org.eclipse.jetty.servlets; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import javax.servlet.RequestDispatcher; import javax.servlet.ServletContext; @@ -27,100 +29,118 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -/** - * Concatenation Servlet - * <p> - * This servlet may be used to concatenate multiple resources into - * a single response. It is intended to be used to load multiple +import org.eclipse.jetty.util.URIUtil; + +/** + * <p>This servlet may be used to concatenate multiple resources into + * a single response.</p> + * <p>It is intended to be used to load multiple * javascript or css files, but may be used for any content of the - * same mime type that can be meaningfully concatenated. - * <p> - * The servlet uses {@link RequestDispatcher#include(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} + * same mime type that can be meaningfully concatenated.</p> + * <p>The servlet uses {@link RequestDispatcher#include(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} * to combine the requested content, so dynamically generated content - * may be combined (Eg engine.js for DWR). - * <p> - * The servlet uses parameter names of the query string as resource names - * relative to the context root. So these script tags: + * may be combined (Eg engine.js for DWR).</p> + * <p>The servlet uses parameter names of the query string as resource names + * relative to the context root. So these script tags:</p> * <pre> - * <script type="text/javascript" src="../js/behaviour.js"></script> - * <script type="text/javascript" src="../js/ajax.js&/chat/chat.js"></script> - * <script type="text/javascript" src="../chat/chat.js"></script> - * </pre> can be replaced with the single tag (with the ConcatServlet mapped to /concat): + * <script type="text/javascript" src="../js/behaviour.js"></script> + * <script type="text/javascript" src="../js/ajax.js&/chat/chat.js"></script> + * <script type="text/javascript" src="../chat/chat.js"></script> + * </pre> + * <p>can be replaced with the single tag (with the {@code ConcatServlet} + * mapped to {@code /concat}):</p> * <pre> - * <script type="text/javascript" src="../concat?/js/behaviour.js&/js/ajax.js&/chat/chat.js"></script> + * <script type="text/javascript" src="../concat?/js/behaviour.js&/js/ajax.js&/chat/chat.js"></script> * </pre> - * The {@link ServletContext#getMimeType(String)} method is used to determine the - * mime type of each resource. If the types of all resources do not match, then a 415 - * UNSUPPORTED_MEDIA_TYPE error is returned. - * <p> - * If the init parameter "development" is set to "true" then the servlet will run in - * development mode and the content will be concatenated on every request. Otherwise - * the init time of the servlet is used as the lastModifiedTime of the combined content - * and If-Modified-Since requests are handled with 206 NOT Modified responses if + * <p>The {@link ServletContext#getMimeType(String)} method is used to determine the + * mime type of each resource. If the types of all resources do not match, then a 415 + * UNSUPPORTED_MEDIA_TYPE error is returned.</p> + * <p>If the init parameter {@code development} is set to {@code true} then the servlet + * will run in development mode and the content will be concatenated on every request.</p> + * <p>Otherwise the init time of the servlet is used as the lastModifiedTime of the combined content + * and If-Modified-Since requests are handled with 304 NOT Modified responses if * appropriate. This means that when not in development mode, the servlet must be - * restarted before changed content will be served. - * - * - * + * restarted before changed content will be served.</p> */ public class ConcatServlet extends HttpServlet { - boolean _development; - long _lastModified; - ServletContext _context; + private boolean _development; + private long _lastModified; - /* ------------------------------------------------------------ */ + @Override public void init() throws ServletException { - _lastModified=System.currentTimeMillis(); - _context=getServletContext(); - _development="true".equals(getInitParameter("development")); + _lastModified = System.currentTimeMillis(); + _development = Boolean.parseBoolean(getInitParameter("development")); } - /* ------------------------------------------------------------ */ /* * @return The start time of the servlet unless in development mode, in which case -1 is returned. */ + @Override protected long getLastModified(HttpServletRequest req) { - return _development?-1:_lastModified; + return _development ? -1 : _lastModified; } - /* ------------------------------------------------------------ */ - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String q=req.getQueryString(); - if (q==null) + String query = request.getQueryString(); + if (query == null) { - resp.sendError(HttpServletResponse.SC_NO_CONTENT); + response.sendError(HttpServletResponse.SC_NO_CONTENT); return; } - String[] parts = q.split("\\&"); - String type=null; - for (int i=0;i<parts.length;i++) + List<RequestDispatcher> dispatchers = new ArrayList<>(); + String[] parts = query.split("\\&"); + String type = null; + for (String part : parts) { - String t = _context.getMimeType(parts[i]); - if (t!=null) + String path = URIUtil.canonicalPath(URIUtil.decodePath(part)); + if (path == null) + { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + // Verify that the path is not protected. + if (startsWith(path, "/WEB-INF/") || startsWith(path, "/META-INF/")) + { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + String t = getServletContext().getMimeType(path); + if (t != null) { - if (type==null) - type=t; + if (type == null) + { + type = t; + } else if (!type.equals(t)) { - resp.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); + response.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); return; } } + + RequestDispatcher dispatcher = getServletContext().getRequestDispatcher(path); + if (dispatcher != null) + dispatchers.add(dispatcher); } - if (type!=null) - resp.setContentType(type); + if (type != null) + response.setContentType(type); - for (int i=0;i<parts.length;i++) - { - RequestDispatcher dispatcher=_context.getRequestDispatcher(parts[i]); - if (dispatcher!=null) - dispatcher.include(req,resp); - } + for (RequestDispatcher dispatcher : dispatchers) + dispatcher.include(request, response); + } + + private boolean startsWith(String path, String prefix) + { + // Case insensitive match. + return prefix.regionMatches(true, 0, path, 0, prefix.length()); } } diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/server/handler/gzip/GzipDefaultTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/server/handler/gzip/GzipDefaultTest.java index c926bd4c29..96dbf03cbc 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/server/handler/gzip/GzipDefaultTest.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/server/handler/gzip/GzipDefaultTest.java @@ -674,12 +674,13 @@ public class GzipDefaultTest GzipTester tester = new GzipTester(testingdir,compressionType); // Configure Gzip Handler - tester.getGzipHandler().setExcludedPaths("*.txt"); - tester.getGzipHandler().setIncludedPaths("/file.txt"); + tester.getGzipHandler().setExcludedPaths("/bad.txt"); + tester.getGzipHandler().setIncludedPaths("*.txt"); // Prepare server file int filesize = tester.getOutputBufferSize() * 4; tester.prepareServerFile("file.txt",filesize); + tester.prepareServerFile("bad.txt",filesize); // Set content servlet tester.setContentServlet(DefaultServlet.class); @@ -693,6 +694,16 @@ public class GzipDefaultTest { tester.stop(); } + + try + { + tester.start(); + assertIsResponseNotGzipCompressed(tester,"GET","bad.txt",filesize,HttpStatus.OK_200); + } + finally + { + tester.stop(); + } } public HttpTester.Response assertIsResponseNotGzipCompressed(GzipTester tester, String method, String filename, int expectedFilesize, int status) diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/ConcatServletTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/ConcatServletTest.java new file mode 100644 index 0000000000..ad58753db0 --- /dev/null +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/ConcatServletTest.java @@ -0,0 +1,175 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 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.servlets; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.server.LocalConnector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.webapp.WebAppContext; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class ConcatServletTest +{ + private Server server; + private LocalConnector connector; + + @Before + public void prepareServer() throws Exception + { + server = new Server(); + connector = new LocalConnector(server); + server.addConnector(connector); + } + + @After + public void destroy() throws Exception + { + if (server != null) + server.stop(); + } + + @Test + public void testConcatenation() throws Exception + { + String contextPath = ""; + ServletContextHandler context = new ServletContextHandler(server, contextPath); + server.setHandler(context); + String concatPath = "/concat"; + context.addServlet(ConcatServlet.class, concatPath); + ServletHolder resourceServletHolder = new ServletHolder(new HttpServlet() + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + String includedURI = (String)request.getAttribute("javax.servlet.include.request_uri"); + response.getOutputStream().println(includedURI); + } + }); + context.addServlet(resourceServletHolder, "/resource/*"); + server.start(); + + String resource1 = "/resource/one.js"; + String resource2 = "/resource/two.js"; + String uri = contextPath + concatPath + "?" + resource1 + "&" + resource2; + String request = "" + + "GET " + uri + " HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Connection: close\r\n" + + "\r\n"; + String response = connector.getResponses(request); + try (BufferedReader reader = new BufferedReader(new StringReader(response))) + { + while (true) + { + String line = reader.readLine(); + if (line == null) + Assert.fail(); + if (line.trim().isEmpty()) + break; + } + Assert.assertEquals(resource1, reader.readLine()); + Assert.assertEquals(resource2, reader.readLine()); + Assert.assertNull(reader.readLine()); + } + } + + @Test + public void testWEBINFResourceIsNotServed() throws Exception + { + File directoryFile = MavenTestingUtils.getTargetTestingDir(); + Path directoryPath = directoryFile.toPath(); + Path hiddenDirectory = directoryPath.resolve("WEB-INF"); + Files.createDirectories(hiddenDirectory); + Path hiddenResource = hiddenDirectory.resolve("one.js"); + try (OutputStream output = Files.newOutputStream(hiddenResource)) + { + output.write("function() {}".getBytes(StandardCharsets.UTF_8)); + } + + String contextPath = ""; + WebAppContext context = new WebAppContext(server, directoryPath.toString(), contextPath); + server.setHandler(context); + String concatPath = "/concat"; + context.addServlet(ConcatServlet.class, concatPath); + server.start(); + + // Verify that I can get the file programmatically, as required by the spec. + Assert.assertNotNull(context.getServletContext().getResource("/WEB-INF/one.js")); + + // Having a path segment and then ".." triggers a special case + // that the ConcatServlet must detect and avoid. + String uri = contextPath + concatPath + "?/trick/../WEB-INF/one.js"; + String request = "" + + "GET " + uri + " HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Connection: close\r\n" + + "\r\n"; + String response = connector.getResponses(request); + Assert.assertTrue(response.startsWith("HTTP/1.1 404 ")); + + // Make sure ConcatServlet behaves well if it's case insensitive. + uri = contextPath + concatPath + "?/trick/../web-inf/one.js"; + request = "" + + "GET " + uri + " HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Connection: close\r\n" + + "\r\n"; + response = connector.getResponses(request); + Assert.assertTrue(response.startsWith("HTTP/1.1 404 ")); + + // Make sure ConcatServlet behaves well if encoded. + uri = contextPath + concatPath + "?/trick/..%2FWEB-INF%2Fone.js"; + request = "" + + "GET " + uri + " HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Connection: close\r\n" + + "\r\n"; + response = connector.getResponses(request); + Assert.assertTrue(response.startsWith("HTTP/1.1 404 ")); + + // Make sure ConcatServlet cannot see file system files. + uri = contextPath + concatPath + "?/trick/../../" + directoryFile.getName(); + request = "" + + "GET " + uri + " HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Connection: close\r\n" + + "\r\n"; + response = connector.getResponses(request); + Assert.assertTrue(response.startsWith("HTTP/1.1 404 ")); + } +} diff --git a/jetty-util-ajax/pom.xml b/jetty-util-ajax/pom.xml index f2243ab9fe..2a166fb488 100644 --- a/jetty-util-ajax/pom.xml +++ b/jetty-util-ajax/pom.xml @@ -14,8 +14,6 @@ </properties> <build> <plugins> -<!-- ---> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>findbugs-maven-plugin</artifactId> diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/IncludeExclude.java b/jetty-util/src/main/java/org/eclipse/jetty/util/IncludeExclude.java new file mode 100644 index 0000000000..fd20040cab --- /dev/null +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/IncludeExclude.java @@ -0,0 +1,129 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 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.util; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.BiFunction; + + +/** Utility class to maintain a set of inclusions and exclusions. + * <p>Maintains a set of included and excluded elements. The method {@link #matches(Object)} + * will return true IFF the passed object is not in the excluded set AND ( either the + * included set is empty OR the object is in the included set) + * <p>The type of the underlying {@link Set} used may be passed into the + * constructor, so special sets like Servlet PathMap may be used. + * <p> + * @param <ITEM> The type of element + */ +public class IncludeExclude<ITEM> +{ + private final Set<ITEM> _includes; + private final Set<ITEM> _excludes; + private final BiFunction<Set<ITEM>,ITEM, Boolean> _matcher; + + /** + * Default constructor over {@link HashSet} + */ + public IncludeExclude() + { + this(HashSet.class,null); + } + + /** + * Construct an IncludeExclude + * @param setClass The type of {@link Set} to using internally + * @param matcher A function to test if a passed ITEM is matched by the passed SET, or null to use {@link Set#contains(Object)} + */ + public <SET extends Set<ITEM>> IncludeExclude(Class<SET> setClass, BiFunction<SET,ITEM, Boolean> matcher) + { + try + { + _includes = setClass.newInstance(); + _excludes = setClass.newInstance(); + _matcher = (BiFunction<Set<ITEM>,ITEM, Boolean>)matcher; + } + catch (InstantiationException | IllegalAccessException e) + { + throw new RuntimeException(e); + } + } + + public void include(ITEM element) + { + _includes.add(element); + } + + public void include(ITEM... element) + { + for (ITEM e: element) + _includes.add(e); + } + + public void exclude(ITEM element) + { + _excludes.add(element); + } + + public void exclude(ITEM... element) + { + for (ITEM e: element) + _excludes.add(e); + } + + public boolean matches(ITEM e) + { + if (_matcher==null) + { + if (_includes.size()>0 && !_includes.contains(e)) + return false; + return !_excludes.contains(e); + } + if (_includes.size()>0 && !_matcher.apply(_includes,e)) + return false; + return !_matcher.apply(_excludes,e); + } + + public int size() + { + return _includes.size()+_excludes.size(); + } + + public Set<ITEM> getIncluded() + { + return _includes; + } + + public Set<ITEM> getExcluded() + { + return _excludes; + } + + public void clear() + { + _includes.clear(); + _excludes.clear(); + } + + @Override + public String toString() + { + return String.format("%s@%x{i=%s,e=%s,m=%s}",this.getClass().getSimpleName(),hashCode(),_includes,_excludes,_matcher); + } +} diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/RegexSet.java b/jetty-util/src/main/java/org/eclipse/jetty/util/RegexSet.java new file mode 100644 index 0000000000..ff72b039a4 --- /dev/null +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/RegexSet.java @@ -0,0 +1,106 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 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.util; + +import java.util.AbstractSet; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.regex.Pattern; + + +/** + * A Set of Regular expressions strings. + * <p> + * Provides the efficient {@link #matches(String)} method to check for a match against all the combined Regex's + */ +public class RegexSet extends AbstractSet<String> +{ + public static final BiFunction<RegexSet,String,Boolean> MATCHER=(rs,p)->{return rs.matches(p);}; + private final Set<String> _patterns=new HashSet<String>(); + private final Set<String> _unmodifiable=Collections.unmodifiableSet(_patterns); + private Pattern _pattern; + + @Override + public Iterator<String> iterator() + { + return _unmodifiable.iterator(); + } + + @Override + public int size() + { + return _patterns.size(); + } + + @Override + public boolean add(String pattern) + { + boolean added = _patterns.add(pattern); + if (added) + updatePattern(); + return added; + } + + @Override + public boolean remove(Object pattern) + { + boolean removed = _patterns.remove(pattern); + + if (removed) + updatePattern(); + return removed; + } + + @Override + public boolean isEmpty() + { + return _patterns.isEmpty(); + } + + @Override + public void clear() + { + _patterns.clear(); + _pattern=null; + } + + private void updatePattern() + { + StringBuilder builder = new StringBuilder(); + builder.append("^("); + for (String pattern: _patterns) + { + if (builder.length()>2) + builder.append('|'); + builder.append('('); + builder.append(pattern); + builder.append(')'); + } + builder.append(")$"); + _pattern = Pattern.compile(builder.toString()); + } + + public boolean matches(String s) + { + return _pattern!=null && _pattern.matcher(s).matches(); + } +} diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/log/Log.java b/jetty-util/src/main/java/org/eclipse/jetty/util/log/Log.java index b1163e6309..16ba4be4f8 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/log/Log.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/log/Log.java @@ -169,8 +169,8 @@ public class Log try { - Class<?> log_class = Loader.loadClass(Log.class, __logClass); - if (LOG == null || !LOG.getClass().equals(log_class)) + Class<?> log_class = __logClass==null?null:Loader.loadClass(Log.class, __logClass); + if (LOG == null || (log_class!=null && !LOG.getClass().equals(log_class))) { LOG = (Logger)log_class.newInstance(); LOG.debug("Logging to {} via {}", LOG, log_class.getName()); @@ -209,9 +209,16 @@ public class Log return LOG; } + /** + * Set the root logger. + * <p>Note that if any classes have statically obtained their logger instance + * prior to this call, their Logger will not be affected by this call. + * @param log + */ public static void setLog(Logger log) { Log.LOG = log; + __logClass=null; } /** diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java index 35e7ee064f..71c10ac62a 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java @@ -126,7 +126,7 @@ public class PathResource extends Resource } catch (IOException e) { - // Ignore + LOG.ignore(e); } catch (Exception e) { diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/IncludeExcludeTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/IncludeExcludeTest.java new file mode 100644 index 0000000000..8da30fdb38 --- /dev/null +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/IncludeExcludeTest.java @@ -0,0 +1,153 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 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.util; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class IncludeExcludeTest +{ + @Test + public void testEmpty() + { + IncludeExclude<String> ie = new IncludeExclude<>(); + + assertEquals(0,ie.size()); + assertEquals(true,ie.matches("foo")); + } + + @Test + public void testIncludeOnly() + { + IncludeExclude<String> ie = new IncludeExclude<>(); + ie.include("foo"); + ie.include("bar"); + + assertEquals(2,ie.size()); + assertEquals(false,ie.matches("")); + assertEquals(true,ie.matches("foo")); + assertEquals(true,ie.matches("bar")); + assertEquals(false,ie.matches("foobar")); + } + + @Test + public void testExcludeOnly() + { + IncludeExclude<String> ie = new IncludeExclude<>(); + ie.exclude("foo"); + ie.exclude("bar"); + + assertEquals(2,ie.size()); + + assertEquals(false,ie.matches("foo")); + assertEquals(false,ie.matches("bar")); + assertEquals(true,ie.matches("")); + assertEquals(true,ie.matches("foobar")); + assertEquals(true,ie.matches("wibble")); + } + + @Test + public void testIncludeExclude() + { + IncludeExclude<String> ie = new IncludeExclude<>(); + ie.include("foo"); + ie.include("bar"); + ie.exclude("bar"); + ie.exclude("xxx"); + + assertEquals(4,ie.size()); + + assertEquals(true,ie.matches("foo")); + assertEquals(false,ie.matches("bar")); + assertEquals(false,ie.matches("")); + assertEquals(false,ie.matches("foobar")); + assertEquals(false,ie.matches("xxx")); + } + + + + @Test + public void testEmptyRegex() + { + IncludeExclude<String> ie = new IncludeExclude<>(RegexSet.class,RegexSet.MATCHER); + + assertEquals(0,ie.size()); + assertEquals(true,ie.matches("foo")); + } + + @Test + public void testIncludeRegex() + { + IncludeExclude<String> ie = new IncludeExclude<>(RegexSet.class,RegexSet.MATCHER); + ie.include("f.."); + ie.include("b((ar)|(oo))"); + + assertEquals(2,ie.size()); + assertEquals(false,ie.matches("")); + assertEquals(true,ie.matches("foo")); + assertEquals(true,ie.matches("far")); + assertEquals(true,ie.matches("bar")); + assertEquals(true,ie.matches("boo")); + assertEquals(false,ie.matches("foobar")); + assertEquals(false,ie.matches("xxx")); + } + + @Test + public void testExcludeRegex() + { + IncludeExclude<String> ie = new IncludeExclude<>(RegexSet.class,RegexSet.MATCHER); + ie.exclude("f.."); + ie.exclude("b((ar)|(oo))"); + + assertEquals(2,ie.size()); + + assertEquals(false,ie.matches("foo")); + assertEquals(false,ie.matches("far")); + assertEquals(false,ie.matches("bar")); + assertEquals(false,ie.matches("boo")); + assertEquals(true,ie.matches("")); + assertEquals(true,ie.matches("foobar")); + assertEquals(true,ie.matches("xxx")); + } + + @Test + public void testIncludeExcludeRegex() + { + IncludeExclude<String> ie = new IncludeExclude<>(RegexSet.class,RegexSet.MATCHER); + ie.include(".*[aeiou].*"); + ie.include("[AEIOU].*"); + ie.exclude("f.."); + ie.exclude("b((ar)|(oo))"); + + assertEquals(4,ie.size()); + assertEquals(false,ie.matches("foo")); + assertEquals(false,ie.matches("far")); + assertEquals(false,ie.matches("bar")); + assertEquals(false,ie.matches("boo")); + assertEquals(false,ie.matches("")); + assertEquals(false,ie.matches("xxx")); + + assertEquals(true,ie.matches("foobar")); + assertEquals(true,ie.matches("Ant")); + + } + + +} diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/RegexSetTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/RegexSetTest.java new file mode 100644 index 0000000000..f80c2eda5c --- /dev/null +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/RegexSetTest.java @@ -0,0 +1,86 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 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.util; + +import org.junit.Test; + +import org.junit.Assert; + +public class RegexSetTest +{ + + @Test + public void testEmpty() + { + RegexSet set = new RegexSet(); + + Assert.assertEquals(false,set.contains("foo")); + Assert.assertEquals(false,set.matches("foo")); + Assert.assertEquals(false,set.matches("")); + + } + + @Test + public void testSimple() + { + RegexSet set = new RegexSet(); + set.add("foo.*"); + + Assert.assertEquals(true,set.contains("foo.*")); + Assert.assertEquals(true,set.matches("foo")); + Assert.assertEquals(true,set.matches("foobar")); + Assert.assertEquals(false,set.matches("bar")); + Assert.assertEquals(false,set.matches("")); + + } + + @Test + public void testSimpleTerminated() + { + RegexSet set = new RegexSet(); + set.add("^foo.*$"); + + Assert.assertEquals(true,set.contains("^foo.*$")); + Assert.assertEquals(true,set.matches("foo")); + Assert.assertEquals(true,set.matches("foobar")); + Assert.assertEquals(false,set.matches("bar")); + Assert.assertEquals(false,set.matches("")); + } + + @Test + public void testCombined() + { + RegexSet set = new RegexSet(); + set.add("^foo.*$"); + set.add("bar"); + set.add("[a-z][0-9][a-z][0-9]"); + + Assert.assertEquals(true,set.contains("^foo.*$")); + Assert.assertEquals(true,set.matches("foo")); + Assert.assertEquals(true,set.matches("foobar")); + Assert.assertEquals(true,set.matches("bar")); + Assert.assertEquals(true,set.matches("c3p0")); + Assert.assertEquals(true,set.matches("r2d2")); + + Assert.assertEquals(false,set.matches("wibble")); + Assert.assertEquals(false,set.matches("barfoo")); + Assert.assertEquals(false,set.matches("2b!b")); + Assert.assertEquals(false,set.matches("")); + } +} |