From 4af81674d2d680767553d66fe96571b33ac8e202 Mon Sep 17 00:00:00 2001 From: "S K (xz64)" Date: Tue, 13 Jun 2017 20:58:22 -0700 Subject: [PATCH 1/3] add general purpose header filters Signed-off-by: S K (xz64) --- .../administration/extras/chapter.adoc | 1 + .../administration/extras/header-filter.adoc | 115 ++++++ .../eclipse/jetty/servlets/HeaderFilter.java | 197 ++++++++++ .../servlets/IncludeExcludeBasedFilter.java | 162 ++++++++ .../jetty/servlets/HeaderFilterTest.java | 137 +++++++ .../IncludeExcludeBasedFilterTest.java | 353 ++++++++++++++++++ 6 files changed, 965 insertions(+) create mode 100644 jetty-documentation/src/main/asciidoc/administration/extras/header-filter.adoc create mode 100644 jetty-servlets/src/main/java/org/eclipse/jetty/servlets/HeaderFilter.java create mode 100644 jetty-servlets/src/main/java/org/eclipse/jetty/servlets/IncludeExcludeBasedFilter.java create mode 100644 jetty-servlets/src/test/java/org/eclipse/jetty/servlets/HeaderFilterTest.java create mode 100644 jetty-servlets/src/test/java/org/eclipse/jetty/servlets/IncludeExcludeBasedFilterTest.java diff --git a/jetty-documentation/src/main/asciidoc/administration/extras/chapter.adoc b/jetty-documentation/src/main/asciidoc/administration/extras/chapter.adoc index 9e3a200c29c..b44a7588c64 100644 --- a/jetty-documentation/src/main/asciidoc/administration/extras/chapter.adoc +++ b/jetty-documentation/src/main/asciidoc/administration/extras/chapter.adoc @@ -30,6 +30,7 @@ include::balancer-servlet.adoc[] include::cgi-servlet.adoc[] include::qos-filter.adoc[] include::dos-filter.adoc[] +include::header-filter.adoc[] include::gzip-filter.adoc[] include::cross-origin-filter.adoc[] include::resource-handler.adoc[] diff --git a/jetty-documentation/src/main/asciidoc/administration/extras/header-filter.adoc b/jetty-documentation/src/main/asciidoc/administration/extras/header-filter.adoc new file mode 100644 index 00000000000..e7e97d9079e --- /dev/null +++ b/jetty-documentation/src/main/asciidoc/administration/extras/header-filter.adoc @@ -0,0 +1,115 @@ +// ======================================================================== +// Copyright (c) 1995-2017 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. +// ======================================================================== + +[[header-filter]] +=== Header Filter + +[[header-filter-metadata]] +==== Info + +* Classname: `org.eclipse.jetty.servlets.HeaderFilter` +* Maven Artifact: org.eclipse.jetty:jetty-servlets +* Javadoc: {JDURL}/org/eclipse/jetty/servlets/HeaderFilter.html +* Xref: {JXURL}/org/eclipse/jetty/servlets/HeaderFilter.html + +[[header-filter-usage]] +==== Usage + +The header filter sets or adds headers to each response based on an optionally included/excluded list of path specs, mime types, and/or HTTP methods. + +===== Required JARs + +To use the Header Filter, these JAR files must be available in WEB-INF/lib: + +* $JETTY_HOME/lib/jetty-http.jar +* $JETTY_HOME/lib/jetty-servlets.jar +* $JETTY_HOME/lib/jetty-util.jar + +===== Sample Configuration + +Place the configuration in a webapp's `web.xml` or `jetty-web.xml`. +This filter will perform the following actions on each response: + +* Set the X-Frame-Options header to DENY. +* Add a Cache-Control header containing no-cache, no-store, must-revalidate +* Set the Expires header to approximately one year in the future. +* Add a Date header with the current system time. + +____ +[NOTE] +Each action must be separated by a comma. +____ + +[source, xml, subs="{sub-order}"] +---- + + HeaderFilter + org.eclipse.jetty.servlets.HeaderFilter + + headerConfig + + set X-Frame-Options: DENY, + "add Cache-Control: no-cache, no-store, must-revalidate", + setDate Expires: 31540000000, + addDate Date: 0 + + + +---- + +[[header-filter-init]] +===== Configuring Header Filter Parameters + +The following `init` parameters control the behavior of the filter: + +includedPaths:: +Optional. CSV of included path specs. + +excludedPaths:: +Optional. CSV of excluded path specs. + +includedMimeTypes:: +Optional. CSV of included mime types. + +excludedMimeTypes:: +Optional. CSV of excluded mime types. + +includedHttpMethods:: +Optional. CSV of included http methods. + +excludedHttpMethods:: +Optional. CSV of excluded http methods. + +headerConfig:: +CSV of actions to perform on headers. The syntax for each action is `action headerName: headerValue`. + +Supported header actions: + +* `set` - causes set `setHeader` to be called on the response +* `add` - causes set `addHeader` to be called on the response +* `setDate` - causes `setDateHeader` to be called on the response. +* `addDate` - causes `addDateHeader` to be called on the response. + +If `setDate` or `addDate` is used, `headerValue` should be the number of milliseconds to add to the current system time before writing the header value. + +If a property is both included and excluded by the filter configuration, then it will be considered excluded. + +Path spec rules: + +* If the spec starts with `^`, the spec is assumed to be a regex based path spec and will match with normal Java regex rules. +* If the spec starts with `/`, the spec is assumed to be a Servlet url-pattern rules path spec for either an exact match or prefix based match. +* If the spec starts with `*.`, the spec is assumed to be a Servlet url-pattern rules path spec for a suffix based match. +* All other syntaxes are unsupported. diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/HeaderFilter.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/HeaderFilter.java new file mode 100644 index 00000000000..c23b6b5b88c --- /dev/null +++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/HeaderFilter.java @@ -0,0 +1,197 @@ +// +// ======================================================================== +// Copyright (c) 1995-2017 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.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/** + * Header Filter + *

+ * This filter sets or adds a header to the response. + *

+ * The {@code headerConfig} init param is a CSV of actions to perform on headers, with the following syntax:
+ * [action] [header name]: [header value]
+ * [action] can be one of set, add, setDate, or addDate
+ * The date actions will add the header value in milliseconds to the current system time before setting a date header. + *

+ * Below is an example value for headerConfig:
+ * + *

+ * set X-Frame-Options: DENY,
+ * "add Cache-Control: no-cache, no-store, must-revalidate",
+ * setDate Expires: 31540000000,
+ * addDate Date: 0
+ * 
+ * + * @see IncludeExcludeBasedFilter + */ +public class HeaderFilter extends IncludeExcludeBasedFilter +{ + private List _configuredHeaders = new ArrayList<>(); + private static final Logger LOG = Log.getLogger(HeaderFilter.class); + + @Override + public void init(FilterConfig filterConfig) throws ServletException + { + super.init(filterConfig); + String header_config = filterConfig.getInitParameter("headerConfig"); + + if (header_config != null) + { + String[] configs = StringUtil.csvSplit(header_config); + for (String config : configs) + _configuredHeaders.add(parseHeaderConfiguration(config)); + } + + if (LOG.isDebugEnabled()) + LOG.debug(this.toString()); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException + { + chain.doFilter(request,response); + + HttpServletRequest http_request = (HttpServletRequest)request; + HttpServletResponse http_response = (HttpServletResponse)response; + + if (!super.shouldFilter(http_request,http_response)) + { + return; + } + + for (ConfiguredHeader header : _configuredHeaders) + { + if (header.isDate()) + { + long header_value = System.currentTimeMillis() + header.getMsOffset(); + if (header.isAdd()) + { + http_response.addDateHeader(header.getName(),header_value); + } + else + { + http_response.setDateHeader(header.getName(),header_value); + } + } + else // constant header value + { + if (header.isAdd()) + { + http_response.addHeader(header.getName(),header.getValue()); + } + else + { + http_response.setHeader(header.getName(),header.getValue()); + } + } + } + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append(super.toString()).append("\n"); + sb.append("configured headers:\n"); + for (ConfiguredHeader c : _configuredHeaders) + sb.append(c).append("\n"); + + return sb.toString(); + } + + private ConfiguredHeader parseHeaderConfiguration(String config) + { + String[] config_tokens = config.trim().split(" ",2); + String method = config_tokens[0].trim(); + String header = config_tokens[1]; + String[] header_tokens = header.trim().split(":",2); + String header_name = header_tokens[0].trim(); + String header_value = header_tokens[1].trim(); + ConfiguredHeader configured_header = new ConfiguredHeader(header_name,header_value,method.startsWith("add"),method.endsWith("Date")); + return configured_header; + } + + private static class ConfiguredHeader + { + private String _name; + private String _value; + private long _msOffset; + private boolean _add; + private boolean _date; + + public ConfiguredHeader(String name, String value, boolean add, boolean date) + { + _name = name; + _value = value; + _add = add; + _date = date; + + if (_date) + { + _msOffset = Long.parseLong(_value); + } + } + + public String getName() + { + return _name; + } + + public String getValue() + { + return _value; + } + + public boolean isAdd() + { + return _add; + } + + public boolean isDate() + { + return _date; + } + + public long getMsOffset() + { + return _msOffset; + } + + @Override + public String toString() + { + return (_add?"add":"set") + (_date?"Date":"") + " " + _name + ": " + _value; + } + } +} diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/IncludeExcludeBasedFilter.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/IncludeExcludeBasedFilter.java new file mode 100644 index 00000000000..b1ba2cf4342 --- /dev/null +++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/IncludeExcludeBasedFilter.java @@ -0,0 +1,162 @@ +// +// ======================================================================== +// Copyright (c) 1995-2017 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 javax.servlet.Filter; +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.http.pathmap.PathSpecSet; +import org.eclipse.jetty.util.IncludeExclude; +import org.eclipse.jetty.util.IncludeExcludeSet; +import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.URIUtil; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/** + * Include Exclude Based Filter + *

+ * This is an abstract filter which helps with filtering based on include/exclude of paths, mime types, and/or http methods. + *

+ * Use the {@link #shouldFilter(HttpServletRequest, HttpServletResponse)} method to determine if a request/response should be filtered. If mime types are used, + * it should be called after {@link javax.servlet.FilterChain#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} since the mime type may not + * be written until then. + * + * Supported init params: + *

    + *
  • includedPaths - CSV of path specs to include
  • + *
  • excludedPaths - CSV of path specs to exclude
  • + *
  • includedMimeTypes - CSV of mime types to include
  • + *
  • excludedMimeTypes - CSV of mime types to exclude
  • + *
  • includedHttpMethods - CSV of http methods to include
  • + *
  • excludedHttpMethods - CSV of http methods to exclude
  • + *
+ *

+ * Path spec rules: + *

    + *
  • If the spec starts with '^' the spec is assumed to be a regex based path spec and will match with normal Java regex rules.
  • + *
  • If the spec starts with '/' the spec is assumed to be a Servlet url-pattern rules path spec for either an exact match or prefix based + * match.
  • + *
  • If the spec starts with '*.' the spec is assumed to be a Servlet url-pattern rules path spec for a suffix based match.
  • + *
  • All other syntaxes are unsupported.
  • + *
+ *

+ * CSVs are parsed with {@link StringUtil#csvSplit(String)} + * + * @see PathSpecSet + * @see IncludeExcludeSet + */ +public abstract class IncludeExcludeBasedFilter implements Filter +{ + private final IncludeExclude _mimeTypes = new IncludeExclude<>(); + private final IncludeExclude _httpMethods = new IncludeExclude<>(); + private final IncludeExclude _paths = new IncludeExclude<>(PathSpecSet.class); + private static final Logger LOG = Log.getLogger(IncludeExcludeBasedFilter.class); + + @Override + public void init(FilterConfig filterConfig) throws ServletException + { + String included_paths = filterConfig.getInitParameter("includedPaths"); + String excluded_paths = filterConfig.getInitParameter("excludedPaths"); + String included_mime_types = filterConfig.getInitParameter("includedMimeTypes"); + String excluded_mime_types = filterConfig.getInitParameter("excludedMimeTypes"); + String included_http_methods = filterConfig.getInitParameter("includedHttpMethods"); + String excluded_http_methods = filterConfig.getInitParameter("excludedHttpMethods"); + + if (included_paths != null) + { + _paths.include(StringUtil.csvSplit(included_paths)); + } + if (excluded_paths != null) + { + _paths.exclude(StringUtil.csvSplit(excluded_paths)); + } + if (included_mime_types != null) + { + _mimeTypes.include(StringUtil.csvSplit(included_mime_types)); + } + if (excluded_mime_types != null) + { + _mimeTypes.exclude(StringUtil.csvSplit(excluded_mime_types)); + } + if (included_http_methods != null) + { + _httpMethods.include(StringUtil.csvSplit(included_http_methods)); + } + if (excluded_http_methods != null) + { + _httpMethods.exclude(StringUtil.csvSplit(excluded_http_methods)); + } + } + + protected boolean shouldFilter(HttpServletRequest http_request, HttpServletResponse http_response) + { + String http_method = http_request.getMethod(); + LOG.debug("HTTP method is: {}",http_method); + if (!_httpMethods.test(http_method)) + { + LOG.debug("should not apply filter because HTTP method does not match"); + return false; + } + + String content_type = http_response.getContentType(); + LOG.debug("Content Type is: {}",content_type); + content_type = (content_type == null)?"":content_type; + String mime_type = MimeTypes.getContentTypeWithoutCharset(content_type); + + LOG.debug("Mime Type is: {}",content_type); + if (!_mimeTypes.test(mime_type)) + { + LOG.debug("should not apply filter because mime type does not match"); + return false; + } + + ServletContext context = http_request.getServletContext(); + String path = context == null?http_request.getRequestURI():URIUtil.addPaths(http_request.getServletPath(),http_request.getPathInfo()); + LOG.debug("Path is: {}",path); + if (!_paths.test(path)) + { + LOG.debug("should not apply filter because path does not match"); + return false; + } + + return true; + } + + @Override + public void destroy() + { + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("filter configuration:\n"); + sb.append("paths:\n").append(_paths).append("\n"); + sb.append("mime types:\n").append(_mimeTypes).append("\n"); + sb.append("http methods:\n").append(_httpMethods); + return sb.toString(); + } +} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/HeaderFilterTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/HeaderFilterTest.java new file mode 100644 index 00000000000..5230a96fd24 --- /dev/null +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/HeaderFilterTest.java @@ -0,0 +1,137 @@ +// +// ======================================================================== +// Copyright (c) 1995-2017 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.IOException; +import java.util.EnumSet; + +import javax.servlet.DispatcherType; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpTester; +import org.eclipse.jetty.servlet.FilterHolder; +import org.eclipse.jetty.servlet.ServletTester; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class HeaderFilterTest +{ + private ServletTester _tester; + + @Before + public void setUp() throws Exception + { + _tester = new ServletTester(); + _tester.setContextPath("/context"); + _tester.addServlet(NullServlet.class,"/test/*"); + + _tester.start(); + } + + @After + public void tearDown() throws Exception + { + _tester.stop(); + } + + @Test + public void testHeaderFilterSet() throws Exception + { + FilterHolder holder = new FilterHolder(HeaderFilter.class); + holder.setInitParameter("headerConfig","set X-Frame-Options: DENY"); + _tester.getContext().getServletHandler().addFilterWithMapping(holder,"/*",EnumSet.of(DispatcherType.REQUEST)); + + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion("HTTP/1.1"); + request.setHeader("Host","localhost"); + request.setURI("/context/test/0"); + + HttpTester.Response response = HttpTester.parseResponse(_tester.getResponses(request.generate())); + Assert.assertTrue(response.contains("X-Frame-Options","DENY")); + } + + @Test + public void testHeaderFilterAdd() throws Exception + { + FilterHolder holder = new FilterHolder(HeaderFilter.class); + holder.setInitParameter("headerConfig","add X-Frame-Options: DENY"); + _tester.getContext().getServletHandler().addFilterWithMapping(holder,"/*",EnumSet.of(DispatcherType.REQUEST)); + + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion("HTTP/1.1"); + request.setHeader("Host","localhost"); + request.setURI("/context/test/0"); + + HttpTester.Response response = HttpTester.parseResponse(_tester.getResponses(request.generate())); + Assert.assertTrue(response.contains("X-Frame-Options","DENY")); + } + + @Test + public void testHeaderFilterSetDate() throws Exception + { + FilterHolder holder = new FilterHolder(HeaderFilter.class); + holder.setInitParameter("headerConfig","setDate Expires: 100"); + _tester.getContext().getServletHandler().addFilterWithMapping(holder,"/*",EnumSet.of(DispatcherType.REQUEST)); + + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion("HTTP/1.1"); + request.setHeader("Host","localhost"); + request.setURI("/context/test/0"); + + HttpTester.Response response = HttpTester.parseResponse(_tester.getResponses(request.generate())); + Assert.assertTrue(response.contains(HttpHeader.EXPIRES)); + } + + @Test + public void testHeaderFilterAddDate() throws Exception + { + FilterHolder holder = new FilterHolder(HeaderFilter.class); + holder.setInitParameter("headerConfig","addDate Expires: 100"); + _tester.getContext().getServletHandler().addFilterWithMapping(holder,"/*",EnumSet.of(DispatcherType.REQUEST)); + + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion("HTTP/1.1"); + request.setHeader("Host","localhost"); + request.setURI("/context/test/0"); + + HttpTester.Response response = HttpTester.parseResponse(_tester.getResponses(request.generate())); + Assert.assertTrue(response.contains(HttpHeader.EXPIRES)); + } + + public static class NullServlet extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + resp.setStatus(HttpStatus.NO_CONTENT_204); + } + + } +} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/IncludeExcludeBasedFilterTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/IncludeExcludeBasedFilterTest.java new file mode 100644 index 00000000000..f9971247ad2 --- /dev/null +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/IncludeExcludeBasedFilterTest.java @@ -0,0 +1,353 @@ +// +// ======================================================================== +// Copyright (c) 1995-2017 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.IOException; +import java.util.EnumSet; + +import javax.servlet.DispatcherType; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpTester; +import org.eclipse.jetty.servlet.FilterHolder; +import org.eclipse.jetty.servlet.ServletTester; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class IncludeExcludeBasedFilterTest +{ + private ServletTester _tester; + + @Before + public void setUp() throws Exception + { + _tester = new ServletTester(); + _tester.setContextPath("/context"); + _tester.addServlet(NullServlet.class,"/test/*"); + + _tester.start(); + } + + @After + public void tearDown() throws Exception + { + _tester.stop(); + } + + @Test + public void testIncludeExcludeFilterIncludedPathMatch() throws Exception + { + FilterHolder holder = new FilterHolder(MockIncludeExcludeFilter.class); + holder.setInitParameter("includedPaths","^/test/0$"); + _tester.getContext().getServletHandler().addFilterWithMapping(holder,"/*",EnumSet.of(DispatcherType.REQUEST)); + + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion("HTTP/1.1"); + request.setHeader("Host","localhost"); + request.setURI("/context/test/0"); + + HttpTester.Response response = HttpTester.parseResponse(_tester.getResponses(request.generate())); + Assert.assertTrue(response.contains("X-Custom-Value","1")); + } + + @Test + public void testIncludeExcludeFilterIncludedPathNoMatch() throws Exception + { + FilterHolder holder = new FilterHolder(MockIncludeExcludeFilter.class); + holder.setInitParameter("includedPaths","^/nomatchtest$"); + _tester.getContext().getServletHandler().addFilterWithMapping(holder,"/*",EnumSet.of(DispatcherType.REQUEST)); + + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion("HTTP/1.1"); + request.setHeader("Host","localhost"); + request.setURI("/context/test/0"); + + HttpTester.Response response = HttpTester.parseResponse(_tester.getResponses(request.generate())); + Assert.assertFalse(response.contains("X-Custom-Value","1")); + } + + @Test + public void testIncludeExcludeFilterExcludedPathMatch() throws Exception + { + FilterHolder holder = new FilterHolder(MockIncludeExcludeFilter.class); + holder.setInitParameter("excludedPaths","^/test/0$"); + _tester.getContext().getServletHandler().addFilterWithMapping(holder,"/*",EnumSet.of(DispatcherType.REQUEST)); + + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion("HTTP/1.1"); + request.setHeader("Host","localhost"); + request.setURI("/context/test/0"); + + HttpTester.Response response = HttpTester.parseResponse(_tester.getResponses(request.generate())); + Assert.assertFalse(response.contains("X-Custom-Value","1")); + } + + @Test + public void testIncludeExcludeFilterExcludedPathNoMatch() throws Exception + { + FilterHolder holder = new FilterHolder(MockIncludeExcludeFilter.class); + holder.setInitParameter("excludedPaths","^/nomatchtest$"); + _tester.getContext().getServletHandler().addFilterWithMapping(holder,"/*",EnumSet.of(DispatcherType.REQUEST)); + + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion("HTTP/1.1"); + request.setHeader("Host","localhost"); + request.setURI("/context/test/0"); + + HttpTester.Response response = HttpTester.parseResponse(_tester.getResponses(request.generate())); + Assert.assertTrue(response.contains("X-Custom-Value","1")); + } + + @Test + public void testIncludeExcludeFilterExcludeOverridesInclude() throws Exception + { + FilterHolder holder = new FilterHolder(MockIncludeExcludeFilter.class); + holder.setInitParameter("includedPaths","^/test/0$"); + holder.setInitParameter("excludedPaths","^/test/0$"); + _tester.getContext().getServletHandler().addFilterWithMapping(holder,"/*",EnumSet.of(DispatcherType.REQUEST)); + + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion("HTTP/1.1"); + request.setHeader("Host","localhost"); + request.setURI("/context/test/0"); + + HttpTester.Response response = HttpTester.parseResponse(_tester.getResponses(request.generate())); + Assert.assertFalse(response.contains("X-Custom-Value","1")); + } + + @Test + public void testIncludeExcludeFilterIncludeMethodMatch() throws Exception + { + FilterHolder holder = new FilterHolder(MockIncludeExcludeFilter.class); + holder.setInitParameter("includedHttpMethods","GET"); + _tester.getContext().getServletHandler().addFilterWithMapping(holder,"/*",EnumSet.of(DispatcherType.REQUEST)); + + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion("HTTP/1.1"); + request.setHeader("Host","localhost"); + request.setURI("/context/test/0"); + + HttpTester.Response response = HttpTester.parseResponse(_tester.getResponses(request.generate())); + Assert.assertTrue(response.contains("X-Custom-Value","1")); + } + + @Test + public void testIncludeExcludeFilterIncludeMethodNoMatch() throws Exception + { + FilterHolder holder = new FilterHolder(MockIncludeExcludeFilter.class); + holder.setInitParameter("includedHttpMethods","POST,PUT"); + _tester.getContext().getServletHandler().addFilterWithMapping(holder,"/*",EnumSet.of(DispatcherType.REQUEST)); + + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion("HTTP/1.1"); + request.setHeader("Host","localhost"); + request.setURI("/context/test/0"); + + HttpTester.Response response = HttpTester.parseResponse(_tester.getResponses(request.generate())); + Assert.assertFalse(response.contains("X-Custom-Value","1")); + } + + @Test + public void testIncludeExcludeFilterExcludeMethodMatch() throws Exception + { + FilterHolder holder = new FilterHolder(MockIncludeExcludeFilter.class); + holder.setInitParameter("excludedHttpMethods","GET"); + _tester.getContext().getServletHandler().addFilterWithMapping(holder,"/*",EnumSet.of(DispatcherType.REQUEST)); + + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion("HTTP/1.1"); + request.setHeader("Host","localhost"); + request.setURI("/context/test/0"); + + HttpTester.Response response = HttpTester.parseResponse(_tester.getResponses(request.generate())); + Assert.assertFalse(response.contains("X-Custom-Value","1")); + } + + @Test + public void testIncludeExcludeFilterExcludeMethodNoMatch() throws Exception + { + FilterHolder holder = new FilterHolder(MockIncludeExcludeFilter.class); + holder.setInitParameter("excludedHttpMethods","POST,PUT"); + _tester.getContext().getServletHandler().addFilterWithMapping(holder,"/*",EnumSet.of(DispatcherType.REQUEST)); + + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion("HTTP/1.1"); + request.setHeader("Host","localhost"); + request.setURI("/context/test/0"); + + HttpTester.Response response = HttpTester.parseResponse(_tester.getResponses(request.generate())); + Assert.assertTrue(response.contains("X-Custom-Value","1")); + } + + @Test + public void testIncludeExcludeFilterIncludeMimeTypeMatch() throws Exception + { + FilterHolder holder = new FilterHolder(MockIncludeExcludeFilter.class); + holder.setInitParameter("includedMimeTypes","application/json"); + _tester.getContext().getServletHandler().addFilterWithMapping(holder,"/*",EnumSet.of(DispatcherType.REQUEST)); + + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion("HTTP/1.1"); + request.setHeader("Host","localhost"); + request.setURI("/context/test/json"); + + HttpTester.Response response = HttpTester.parseResponse(_tester.getResponses(request.generate())); + Assert.assertTrue(response.contains("X-Custom-Value","1")); + } + + @Test + public void testIncludeExcludeFilterIncludeMimeTypeNoMatch() throws Exception + { + FilterHolder holder = new FilterHolder(MockIncludeExcludeFilter.class); + holder.setInitParameter("includedMimeTypes","application/xml"); + _tester.getContext().getServletHandler().addFilterWithMapping(holder,"/*",EnumSet.of(DispatcherType.REQUEST)); + + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion("HTTP/1.1"); + request.setHeader("Host","localhost"); + request.setURI("/context/test/json"); + + HttpTester.Response response = HttpTester.parseResponse(_tester.getResponses(request.generate())); + Assert.assertFalse(response.contains("X-Custom-Value","1")); + } + + @Test + public void testIncludeExcludeFilterExcludeMimeTypeMatch() throws Exception + { + FilterHolder holder = new FilterHolder(MockIncludeExcludeFilter.class); + holder.setInitParameter("excludedMimeTypes","application/json"); + _tester.getContext().getServletHandler().addFilterWithMapping(holder,"/*",EnumSet.of(DispatcherType.REQUEST)); + + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion("HTTP/1.1"); + request.setHeader("Host","localhost"); + request.setURI("/context/test/json"); + + HttpTester.Response response = HttpTester.parseResponse(_tester.getResponses(request.generate())); + Assert.assertFalse(response.contains("X-Custom-Value","1")); + } + + @Test + public void testIncludeExcludeFilterExcludeMimeTypeNoMatch() throws Exception + { + FilterHolder holder = new FilterHolder(MockIncludeExcludeFilter.class); + holder.setInitParameter("excludedMimeTypes","application/xml"); + _tester.getContext().getServletHandler().addFilterWithMapping(holder,"/*",EnumSet.of(DispatcherType.REQUEST)); + + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion("HTTP/1.1"); + request.setHeader("Host","localhost"); + request.setURI("/context/test/json"); + + HttpTester.Response response = HttpTester.parseResponse(_tester.getResponses(request.generate())); + Assert.assertTrue(response.contains("X-Custom-Value","1")); + } + + @Test + public void testIncludeExcludeFilterIncludeMimeTypeSemicolonMatch() throws Exception + { + FilterHolder holder = new FilterHolder(MockIncludeExcludeFilter.class); + holder.setInitParameter("includedMimeTypes","application/json"); + _tester.getContext().getServletHandler().addFilterWithMapping(holder,"/*",EnumSet.of(DispatcherType.REQUEST)); + + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion("HTTP/1.1"); + request.setHeader("Host","localhost"); + request.setURI("/context/test/json-utf8"); + + HttpTester.Response response = HttpTester.parseResponse(_tester.getResponses(request.generate())); + Assert.assertTrue(response.contains("X-Custom-Value","1")); + } + + @Test + public void testIncludeExcludeFilterIncludeMimeTypeSemicolonNoMatch() throws Exception + { + FilterHolder holder = new FilterHolder(MockIncludeExcludeFilter.class); + holder.setInitParameter("includedMimeTypes","application/xml"); + _tester.getContext().getServletHandler().addFilterWithMapping(holder,"/*",EnumSet.of(DispatcherType.REQUEST)); + + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion("HTTP/1.1"); + request.setHeader("Host","localhost"); + request.setURI("/context/test/json-utf8"); + + HttpTester.Response response = HttpTester.parseResponse(_tester.getResponses(request.generate())); + Assert.assertFalse(response.contains("X-Custom-Value","1")); + } + + public static class MockIncludeExcludeFilter extends IncludeExcludeBasedFilter + { + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException + { + chain.doFilter(request,response); + HttpServletRequest http_request = (HttpServletRequest)request; + HttpServletResponse http_response = (HttpServletResponse)response; + + if (!super.shouldFilter(http_request,http_response)) + { + return; + } + + http_response.setHeader("X-Custom-Value","1"); + } + } + + public static class NullServlet extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + if (req.getPathInfo().equals("/json")) + { + resp.setContentType("application/json"); + } + else if (req.getPathInfo().equals("/json-utf8")) + { + resp.setContentType("application/json; charset=utf-8"); + } + resp.setStatus(HttpStatus.NO_CONTENT_204); + } + + } +} From 43f778946fc3d7156bd25874037e6000afec4285 Mon Sep 17 00:00:00 2001 From: olivier lamy Date: Fri, 16 Jun 2017 10:46:21 +1000 Subject: [PATCH 2/3] use outputDirectory from reactor projects rather than having to install dependencies first #1623 Signed-off-by: olivier lamy --- jetty-maven-plugin/pom.xml | 6 ++ .../jetty/maven/plugin/AbstractJettyMojo.java | 2 +- .../jetty/maven/plugin/JettyRunMojo.java | 97 +++++++++++++++---- .../maven/plugin/JettyWebAppContext.java | 7 +- 4 files changed, 90 insertions(+), 22 deletions(-) diff --git a/jetty-maven-plugin/pom.xml b/jetty-maven-plugin/pom.xml index cee4d037f11..aad17845263 100644 --- a/jetty-maven-plugin/pom.xml +++ b/jetty-maven-plugin/pom.xml @@ -71,6 +71,12 @@ + + org.apache.maven.plugin-tools + maven-plugin-annotations + ${pluginToolsVersion} + provided + org.eclipse.jetty jetty-util diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/AbstractJettyMojo.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/AbstractJettyMojo.java index 09b1c399646..72b8bad3a95 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/AbstractJettyMojo.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/AbstractJettyMojo.java @@ -341,7 +341,7 @@ public abstract class AbstractJettyMojo extends AbstractMojo { try { - List provided = new ArrayList(); + List provided = new ArrayList<>(); URL[] urls = null; for ( Iterator iter = projectArtifacts.iterator(); iter.hasNext(); ) diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunMojo.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunMojo.java index 0fe35ff1896..259cb0f54d0 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunMojo.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunMojo.java @@ -18,24 +18,29 @@ package org.eclipse.jetty.maven.plugin; +import org.apache.maven.artifact.Artifact; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; +import org.codehaus.plexus.util.StringUtils; +import org.eclipse.jetty.util.PathWatcher; +import org.eclipse.jetty.util.PathWatcher.PathWatchEvent; +import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.webapp.WebAppContext; + import java.io.File; import java.io.IOException; import java.net.URL; +import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; -import org.apache.maven.artifact.Artifact; -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.MojoFailureException; -import org.eclipse.jetty.util.PathWatcher; -import org.eclipse.jetty.util.PathWatcher.PathWatchEvent; -import org.eclipse.jetty.util.resource.Resource; -import org.eclipse.jetty.webapp.WebAppContext; - /** * This goal is used in-situ on a Maven project without first requiring that the project * is assembled into a war, saving time during the development cycle. @@ -154,18 +159,20 @@ public class JettyRunMojo extends AbstractJettyMojo * List of deps that are wars */ protected List warArtifacts; - - - - - - + + @Parameter(defaultValue = "${reactorProjects}", readonly = true, required = true) + private List reactorProjects; + /** * @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#execute() */ @Override public void execute() throws MojoExecutionException, MojoFailureException { + if ( !"war".equals( project.getPackaging() ) || skip ) + { + return; + } warPluginInfo = new WarPluginInfo(project); super.execute(); } @@ -273,7 +280,8 @@ public class JettyRunMojo extends AbstractJettyMojo webApp.setClasses (classesDirectory); if (useTestScope && (testClassesDirectory != null)) webApp.setTestClasses (testClassesDirectory); - + + webApp.getClassPathFiles().addAll( getDependencyProjects() ); webApp.setWebInfLib (getDependencyFiles()); //get copy of a list of war artifacts @@ -556,18 +564,22 @@ public class JettyRunMojo extends AbstractJettyMojo /** * @return */ - private List getDependencyFiles () + private List getDependencyFiles() { List dependencyFiles = new ArrayList(); for ( Iterator iter = projectArtifacts.iterator(); iter.hasNext(); ) { - Artifact artifact = (Artifact) iter.next(); + Artifact artifact = iter.next(); // Include runtime and compile time libraries, and possibly test libs too if(artifact.getType().equals("war")) { continue; } + if (getProjectReferences( artifact, project )!=null) + { + continue; + } if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope())) continue; //never add dependencies of scope=provided to the webapp's classpath (see also param) @@ -581,6 +593,57 @@ public class JettyRunMojo extends AbstractJettyMojo return dependencyFiles; } + + private List getDependencyProjects() + { + List dependencyFiles = new ArrayList(); + for ( Iterator iter = projectArtifacts.iterator(); iter.hasNext(); ) + { + Artifact artifact = iter.next(); + + // Include runtime and compile time libraries, and possibly test libs too + if(artifact.getType().equals("war")) + { + continue; + } + + if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope())) + continue; //never add dependencies of scope=provided to the webapp's classpath (see also param) + + if (Artifact.SCOPE_TEST.equals(artifact.getScope()) && !useTestScope) + continue; //only add dependencies of scope=test if explicitly required + + MavenProject mavenProject = getProjectReferences( artifact, project ); + if (mavenProject != null) + { + dependencyFiles.add( Paths.get(mavenProject.getBuild().getOutputDirectory()).toFile() ); + getLog().debug( "Adding project reference " + mavenProject.getBuild().getOutputDirectory() + + " for WEB-INF/classes " ); + } + } + + return dependencyFiles; + } + + + private MavenProject getProjectReferences( Artifact artifact, MavenProject project ) + { + if ( project.getProjectReferences() == null || project.getProjectReferences().isEmpty() ) + { + return null; + } + Collection mavenProjects = project.getProjectReferences().values(); + for ( MavenProject mavenProject : mavenProjects ) + { + if ( StringUtils.equals( mavenProject.getId(), artifact.getId() ) ) + { + return mavenProject; + } + } + return null; + } + + diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyWebAppContext.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyWebAppContext.java index 638f8bf9996..a27903ceed5 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyWebAppContext.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyWebAppContext.java @@ -95,10 +95,10 @@ public class JettyWebAppContext extends WebAppContext private File _classes = null; private File _testClasses = null; - private final List _webInfClasses = new ArrayList(); - private final List _webInfJars = new ArrayList(); + private final List _webInfClasses = new ArrayList<>(); + private final List _webInfJars = new ArrayList<>(); private final Map _webInfJarMap = new HashMap(); - private List _classpathFiles; //webInfClasses+testClasses+webInfJars + private List _classpathFiles = new ArrayList<>(); //webInfClasses+testClasses+webInfJars private String _jettyEnvXml; private List _overlays; private Resource _quickStartWebXml; @@ -411,7 +411,6 @@ public class JettyWebAppContext extends WebAppContext _webInfClasses.add(_classes); // Set up the classpath - _classpathFiles = new ArrayList(); _classpathFiles.addAll(_webInfClasses); _classpathFiles.addAll(_webInfJars); From 9e0fd7faa0217726ec8b3fac288b2daf17608f45 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Fri, 16 Jun 2017 14:23:29 -0700 Subject: [PATCH 3/3] Issue #1625 - Support new IANA declared websocket close status codes --- .../jetty/websocket/api/StatusCode.java | 33 ++++++++++- .../websocket/client/SlowServerTest.java | 4 +- .../jetty/websocket/common/CloseInfo.java | 37 ++++++++---- .../jetty/websocket/common/CloseInfoTest.java | 19 ++----- .../websocket/server/ab/TestABCase3.java | 5 -- .../server/ab/TestABCase7_BadStatusCodes.java | 16 +++--- .../ab/TestABCase7_GoodStatusCodes.java | 5 +- .../test/resources/jetty-logging.properties | 56 +++++++++---------- 8 files changed, 106 insertions(+), 69 deletions(-) diff --git a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/StatusCode.java b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/StatusCode.java index 4dfe06c906b..3b4d8df23ff 100644 --- a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/StatusCode.java +++ b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/StatusCode.java @@ -22,7 +22,7 @@ package org.eclipse.jetty.websocket.api; * The RFC 6455 specified status codes and IANA: WebSocket Close Code Number Registry */ -public class StatusCode +public final class StatusCode { /** * 1000 indicates a normal closure, meaning that the purpose for which the connection was established has been fulfilled. @@ -137,6 +137,13 @@ public class StatusCode */ public final static int TRY_AGAIN_LATER = 1013; + /** + * 1014 indicates that a gateway or proxy received and invalid upstream response. + *

+ * See [hybi] WebSocket Subprotocol Close Code: Bad Gateway + */ + public final static int INVALID_UPSTREAM_RESPONSE = 1014; + /** * 1015 is a reserved value and MUST NOT be set as a status code in a Close control frame by an endpoint. It is designated for use in applications expecting * a status code to indicate that the connection was closed due to a failure to perform a TLS handshake (e.g., the server certificate can't be verified). @@ -144,4 +151,28 @@ public class StatusCode * See RFC 6455, Section 7.4.1 Defined Status Codes. */ public final static int FAILED_TLS_HANDSHAKE = 1015; + + /** + * Test if provided status code can be sent/received on a WebSocket close. + *

+ * This honors the RFC6455 rules and IANA rules. + *

+ * @param statusCode the statusCode to test + * @return true if transmittable + */ + public static boolean isTransmittable(int statusCode) + { + return (statusCode == NORMAL) || + (statusCode == SHUTDOWN) || + (statusCode == PROTOCOL) || + (statusCode == BAD_DATA) || + (statusCode == BAD_PAYLOAD) || + (statusCode == POLICY_VIOLATION) || + (statusCode == MESSAGE_TOO_LARGE) || + (statusCode == REQUIRED_EXTENSION) || + (statusCode == SERVER_ERROR) || + (statusCode == SERVICE_RESTART) || + (statusCode == TRY_AGAIN_LATER) || + (statusCode == INVALID_UPSTREAM_RESPONSE); + } } diff --git a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/SlowServerTest.java b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/SlowServerTest.java index f2d76a2de35..3fae9a28704 100644 --- a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/SlowServerTest.java +++ b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/SlowServerTest.java @@ -18,7 +18,7 @@ package org.eclipse.jetty.websocket.client; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.is; import java.net.URI; import java.util.concurrent.Future; @@ -34,9 +34,11 @@ import org.eclipse.jetty.websocket.common.test.IBlockheadServerConnection; import org.junit.After; import org.junit.Assert; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; +@Ignore("TODO: Flappy Test") public class SlowServerTest { @Rule diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/CloseInfo.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/CloseInfo.java index a411716c38d..0e1b3fc0b77 100644 --- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/CloseInfo.java +++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/CloseInfo.java @@ -33,7 +33,7 @@ import org.eclipse.jetty.websocket.common.frames.CloseFrame; public class CloseInfo { - private int statusCode; + private int statusCode = 0; private byte[] reasonBytes; public CloseInfo() @@ -71,11 +71,7 @@ public class CloseInfo if (validate) { - if ((statusCode < StatusCode.NORMAL) || (statusCode == StatusCode.UNDEFINED) || (statusCode == StatusCode.NO_CLOSE) - || (statusCode == StatusCode.NO_CODE) || ((statusCode > 1011) && (statusCode <= 2999)) || (statusCode >= 5000)) - { - throw new ProtocolException("Invalid close code: " + statusCode); - } + assertValidStatusCode(statusCode); } if (data.remaining() > 0) @@ -142,6 +138,27 @@ public class CloseInfo } } + private void assertValidStatusCode(int statusCode) + { + // Status Codes outside of RFC6455 defined scope + if ((statusCode <= 999) || (statusCode >= 5000)) + { + throw new ProtocolException("Out of range close status code: " + statusCode); + } + + // Status Codes not allowed to exist in a Close frame (per RFC6455) + if ((statusCode == StatusCode.NO_CLOSE) || (statusCode == StatusCode.NO_CODE) || (statusCode == StatusCode.FAILED_TLS_HANDSHAKE)) + { + throw new ProtocolException("Frame forbidden close status code: " + statusCode); + } + + // Status Code is in defined "reserved space" and is declared (all others are invalid) + if ((statusCode >= 1000) && (statusCode <= 2999) && !StatusCode.isTransmittable(statusCode)) + { + throw new ProtocolException("RFC6455 and IANA Undefined close status code: " + statusCode); + } + } + private ByteBuffer asByteBuffer() { if ((statusCode == StatusCode.NO_CLOSE) || (statusCode == StatusCode.NO_CODE) || (statusCode == (-1))) @@ -175,12 +192,10 @@ public class CloseInfo { CloseFrame frame = new CloseFrame(); frame.setFin(true); - if ((statusCode >= 1000) && (statusCode != StatusCode.NO_CLOSE) && (statusCode != StatusCode.NO_CODE)) + // Frame forbidden codes result in no status code (and no reason string) + if ((statusCode != StatusCode.NO_CLOSE) && (statusCode != StatusCode.NO_CODE) && (statusCode != StatusCode.FAILED_TLS_HANDSHAKE)) { - if (statusCode == StatusCode.FAILED_TLS_HANDSHAKE) - { - throw new ProtocolException("Close Frame with status code " + statusCode + " not allowed (per RFC6455)"); - } + assertValidStatusCode(statusCode); frame.setPayload(asByteBuffer()); } return frame; diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/CloseInfoTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/CloseInfoTest.java index 2966271f6a1..67f1d1e3c61 100644 --- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/CloseInfoTest.java +++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/CloseInfoTest.java @@ -22,17 +22,14 @@ import static org.eclipse.jetty.websocket.api.StatusCode.FAILED_TLS_HANDSHAKE; import static org.eclipse.jetty.websocket.api.StatusCode.NORMAL; import static org.eclipse.jetty.websocket.api.StatusCode.NO_CLOSE; import static org.eclipse.jetty.websocket.api.StatusCode.NO_CODE; -import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; import java.nio.ByteBuffer; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.StringUtil; -import org.eclipse.jetty.websocket.api.ProtocolException; import org.eclipse.jetty.websocket.common.frames.CloseFrame; import org.junit.Test; @@ -99,17 +96,11 @@ public class CloseInfoTest assertThat("close.code",close.getStatusCode(),is(FAILED_TLS_HANDSHAKE)); assertThat("close.reason",close.getReason(),nullValue()); - try - { - @SuppressWarnings("unused") - CloseFrame frame = close.asFrame(); - fail("Expected " + ProtocolException.class.getName()); - } - catch (ProtocolException e) - { - // expected path - assertThat("ProtocolException message",e.getMessage(),containsString("not allowed (per RFC6455)")); - } + CloseFrame frame = close.asFrame(); + assertThat("close frame op code",frame.getOpCode(),is(OpCode.CLOSE)); + // should result in no payload + assertThat("close frame has payload",frame.hasPayload(),is(false)); + assertThat("close frame payload length",frame.getPayloadLength(),is(0)); } /** diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/TestABCase3.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/TestABCase3.java index 891088018c1..4172de15563 100644 --- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/TestABCase3.java +++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/TestABCase3.java @@ -22,7 +22,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import org.eclipse.jetty.toolchain.test.AdvancedRunner; import org.eclipse.jetty.util.log.StacklessLogging; import org.eclipse.jetty.websocket.api.StatusCode; import org.eclipse.jetty.websocket.common.CloseInfo; @@ -32,15 +31,11 @@ import org.eclipse.jetty.websocket.common.frames.BinaryFrame; import org.eclipse.jetty.websocket.common.frames.PingFrame; import org.eclipse.jetty.websocket.common.frames.TextFrame; import org.eclipse.jetty.websocket.common.test.Fuzzer; -import org.junit.Ignore; import org.junit.Test; -import org.junit.runner.RunWith; /** * Test various RSV violations */ -@Ignore -@RunWith(AdvancedRunner.class) public class TestABCase3 extends AbstractABCase { /** diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/TestABCase7_BadStatusCodes.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/TestABCase7_BadStatusCodes.java index a914953731a..fc02dc63bb2 100644 --- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/TestABCase7_BadStatusCodes.java +++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/TestABCase7_BadStatusCodes.java @@ -45,7 +45,7 @@ public class TestABCase7_BadStatusCodes extends AbstractABCase { private static final Logger LOG = Log.getLogger(TestABCase7_GoodStatusCodes.class); - @Parameters + @Parameters(name = "{1} / {0}") public static Collection data() { // The various Good UTF8 sequences as a String (hex form) @@ -54,13 +54,13 @@ public class TestABCase7_BadStatusCodes extends AbstractABCase // @formatter:off data.add(new Object[] { "7.9.1", 0 }); data.add(new Object[] { "7.9.2", 999 }); - data.add(new Object[] { "7.9.3", 1004 }); - data.add(new Object[] { "7.9.4", 1005 }); - data.add(new Object[] { "7.9.5", 1006 }); - data.add(new Object[] { "7.9.6", 1012 }); - data.add(new Object[] { "7.9.7", 1013 }); - data.add(new Object[] { "7.9.8", 1014 }); - data.add(new Object[] { "7.9.9", 1015 }); + data.add(new Object[] { "7.9.3", 1004 }); // RFC6455/UNDEFINED + data.add(new Object[] { "7.9.4", 1005 }); // RFC6455/Cannot Be Transmitted + data.add(new Object[] { "7.9.5", 1006 }); // RFC6455/Cannot Be Transmitted + // data.add(new Object[] { "7.9.6", 1012 }); - IANA Defined + // data.add(new Object[] { "7.9.7", 1013 }); - IANA Defined + // data.add(new Object[] { "7.9.8", 1014 }); - IANA Defined + data.add(new Object[] { "7.9.9", 1015 }); // RFC6455/Cannot Be Transmitted data.add(new Object[] { "7.9.10", 1016 }); data.add(new Object[] { "7.9.11", 1100 }); data.add(new Object[] { "7.9.12", 2000 }); diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/TestABCase7_GoodStatusCodes.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/TestABCase7_GoodStatusCodes.java index 7cb50f24b32..9dff95e8451 100644 --- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/TestABCase7_GoodStatusCodes.java +++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/TestABCase7_GoodStatusCodes.java @@ -43,7 +43,7 @@ public class TestABCase7_GoodStatusCodes extends AbstractABCase { private static final Logger LOG = Log.getLogger(TestABCase7_GoodStatusCodes.class); - @Parameters + @Parameters(name = "{1} / {0}") public static Collection data() { // The various Good UTF8 sequences as a String (hex form) @@ -59,6 +59,9 @@ public class TestABCase7_GoodStatusCodes extends AbstractABCase data.add(new Object[] { "7.7.7", 1009 }); data.add(new Object[] { "7.7.8", 1010 }); data.add(new Object[] { "7.7.9", 1011 }); + data.add(new Object[] { "IANA Assigned", 1012 }); + data.add(new Object[] { "IANA Assigned", 1013 }); + data.add(new Object[] { "IANA Assigned", 1014 }); data.add(new Object[] { "7.7.10", 3000 }); data.add(new Object[] { "7.7.11", 3999 }); data.add(new Object[] { "7.7.12", 4000 }); diff --git a/jetty-websocket/websocket-server/src/test/resources/jetty-logging.properties b/jetty-websocket/websocket-server/src/test/resources/jetty-logging.properties index 924d0006cbb..2a711f20999 100644 --- a/jetty-websocket/websocket-server/src/test/resources/jetty-logging.properties +++ b/jetty-websocket/websocket-server/src/test/resources/jetty-logging.properties @@ -1,29 +1,29 @@ -org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog -org.eclipse.jetty.LEVEL=WARN - -# org.eclipse.jetty.io.WriteFlusher.LEVEL=DEBUG -# org.eclipse.jetty.websocket.LEVEL=DEBUG -# org.eclipse.jetty.websocket.LEVEL=INFO -# org.eclipse.jetty.websocket.common.io.LEVEL=DEBUG -# org.eclipse.jetty.websocket.server.ab.LEVEL=DEBUG -# org.eclipse.jetty.websocket.common.Parser.LEVEL=DEBUG -# org.eclipse.jetty.websocket.common.Generator.LEVEL=DEBUG -# org.eclipse.jetty.websocket.server.ab.Fuzzer.LEVEL=DEBUG -# org.eclipse.jetty.websocket.server.blockhead.LEVEL=DEBUG -# org.eclipse.jetty.websocket.server.helper.LEVEL=DEBUG - -# org.eclipse.jetty.websocket.client.io.ConnectPromise.LEVEL=DEBUG -# org.eclipse.jetty.websocket.common.WebSocketSession_OPEN.LEVEL=DEBUG -# org.eclipse.jetty.websocket.common.io.IOState.LEVEL=DEBUG -# org.eclipse.jetty.websocket.common.io.AbstractWebSocketConnection_OPEN.LEVEL=DEBUG - -### Show state changes on BrowserDebugTool -# -- LEAVE THIS AT DEBUG LEVEL -- -org.eclipse.jetty.websocket.server.browser.LEVEL=DEBUG - -### Disabling intentional error out of RFCSocket -org.eclipse.jetty.websocket.server.helper.RFCSocket.LEVEL=OFF - -### Hiding Stack Traces from various test cases -org.eclipse.jetty.websocket.server.ab.ABSocket.STACKS=OFF +org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog +org.eclipse.jetty.LEVEL=WARN + +# org.eclipse.jetty.io.WriteFlusher.LEVEL=DEBUG +# org.eclipse.jetty.websocket.LEVEL=DEBUG +# org.eclipse.jetty.websocket.LEVEL=INFO +# org.eclipse.jetty.websocket.common.io.LEVEL=DEBUG +# org.eclipse.jetty.websocket.server.ab.LEVEL=DEBUG +# org.eclipse.jetty.websocket.common.Parser.LEVEL=DEBUG +# org.eclipse.jetty.websocket.common.Generator.LEVEL=DEBUG +# org.eclipse.jetty.websocket.server.ab.Fuzzer.LEVEL=DEBUG +# org.eclipse.jetty.websocket.server.blockhead.LEVEL=DEBUG +# org.eclipse.jetty.websocket.server.helper.LEVEL=DEBUG + +# org.eclipse.jetty.websocket.client.io.ConnectPromise.LEVEL=DEBUG +# org.eclipse.jetty.websocket.common.WebSocketSession_OPEN.LEVEL=DEBUG +# org.eclipse.jetty.websocket.common.io.IOState.LEVEL=DEBUG +# org.eclipse.jetty.websocket.common.io.AbstractWebSocketConnection_OPEN.LEVEL=DEBUG + +### Show state changes on BrowserDebugTool +# -- LEAVE THIS AT DEBUG LEVEL -- +org.eclipse.jetty.websocket.server.browser.LEVEL=DEBUG + +### Disabling intentional error out of RFCSocket +org.eclipse.jetty.websocket.server.helper.RFCSocket.LEVEL=OFF + +### Hiding Stack Traces from various test cases +org.eclipse.jetty.websocket.server.ab.ABSocket.STACKS=OFF org.eclipse.jetty.websocket.server.WebSocketCloseTest$FastFailSocket.STACKS=OFF \ No newline at end of file