From 3bea1bc93f6ae250ecadec2bf7418fa04c0f160c Mon Sep 17 00:00:00 2001 From: emancipator Date: Wed, 14 Sep 2022 10:23:06 +0800 Subject: [PATCH] CVE-2019-10241 CVE-2019-10241 --- CVE-2020-27216.patch | 454 ++++++- CVE-2020-27223-pre-1.patch | 545 -------- CVE-2020-27223-pre-2.patch | 692 ---------- CVE-2020-27223-pre-3.patch | 518 -------- CVE-2020-27223-pre-4.patch | 1167 ----------------- CVE-2020-27223.patch | 1150 ++++++++++++++-- CVE-2021-28165-1.patch | 36 - CVE-2021-28165-2.patch | 39 - CVE-2021-28165.patch | 533 ++++++++ CVE-2021-28169.patch | 241 ++-- CVE-2021-34428.patch | 659 ++++++++-- ...15.tar.gz => jetty-9.4.16.v20190411.tar.gz | Bin 18868481 -> 18894200 bytes jetty.spec | 22 +- 13 files changed, 2673 insertions(+), 3383 deletions(-) mode change 100644 => 100755 CVE-2020-27216.patch delete mode 100644 CVE-2020-27223-pre-1.patch delete mode 100644 CVE-2020-27223-pre-2.patch delete mode 100644 CVE-2020-27223-pre-3.patch delete mode 100644 CVE-2020-27223-pre-4.patch mode change 100644 => 100755 CVE-2020-27223.patch delete mode 100644 CVE-2021-28165-1.patch delete mode 100644 CVE-2021-28165-2.patch create mode 100755 CVE-2021-28165.patch mode change 100644 => 100755 CVE-2021-28169.patch mode change 100644 => 100755 CVE-2021-34428.patch rename jetty-9.4.15.v20190215.tar.gz => jetty-9.4.16.v20190411.tar.gz (69%) diff --git a/CVE-2020-27216.patch b/CVE-2020-27216.patch old mode 100644 new mode 100755 index d58e913..6b8051a --- a/CVE-2020-27216.patch +++ b/CVE-2020-27216.patch @@ -1,22 +1,17 @@ -From 53e0e0e9b25a6309bf24ee3b10984f4145701edb Mon Sep 17 00:00:00 2001 -From: Joakim Erdfelt -Date: Thu, 15 Oct 2020 17:39:30 -0500 -Subject: [PATCH] Merge pull request from GHSA-g3wg-6mcf-8jj6 +From: Markus Koschany +Date: Sat, 3 Jul 2021 22:05:57 +0200 +Subject: CVE-2020-27216 -* Issue #5451 - Improving temp directory creation. - -+ Using new Files.createTempDirectory() instead - of nonsense around File.createTempFile() - -Signed-off-by: Joakim Erdfelt - -* Fixes #5451 - Restoring File.deleteOnExit +Origin: https://github.com/eclipse/jetty.project/commit/53e0e0e9b25a6309bf24ee3b10984f4145701edb +Origin: https://github.com/eclipse/jetty.project/commit/9ad6beb80543b392c91653f6bfce233fc75b9d5f --- - .../jetty/webapp/WebInfConfiguration.java | 20 +++++++------------ - 1 file changed, 7 insertions(+), 13 deletions(-) + .../eclipse/jetty/webapp/WebInfConfiguration.java | 20 +-- + .../server/session/InfinispanTestSupport.java | 173 +++++++++++++-------- + .../test/java/org/eclipse/jetty/TestServer.java | 54 +++---- + 3 files changed, 135 insertions(+), 112 deletions(-) diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebInfConfiguration.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebInfConfiguration.java -index b94f788..f39432d 100644 +index b94f788..19663bc 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebInfConfiguration.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebInfConfiguration.java @@ -24,6 +24,8 @@ import java.net.URI; @@ -41,9 +36,9 @@ index b94f788..f39432d 100644 - tmpDir.deleteOnExit(); - context.setTempDirectory(tmpDir); + Path tmpDir = Files.createTempDirectory(template.getTempDirectory().getParentFile().toPath(), WebInfConfiguration.getCanonicalNameForWebAppTmpDir(context)); -+ File tmpDirAsFile = tmpDir.toFile(); -+ tmpDirAsFile.deleteOnExit(); -+ context.setTempDirectory(tmpDirAsFile); ++ File tmpDirAsFile = tmpDir.toFile(); ++ tmpDirAsFile.deleteOnExit(); ++ context.setTempDirectory(tmpDirAsFile); } @@ -60,6 +55,423 @@ index b94f788..f39432d 100644 } configureTempDirectory(tmpDir, context); --- -2.23.0 - +diff --git a/tests/test-sessions/test-infinispan-sessions/src/test/java/org/eclipse/jetty/server/session/InfinispanTestSupport.java b/tests/test-sessions/test-infinispan-sessions/src/test/java/org/eclipse/jetty/server/session/InfinispanTestSupport.java +index 57ecb1f..663e8dd 100644 +--- a/tests/test-sessions/test-infinispan-sessions/src/test/java/org/eclipse/jetty/server/session/InfinispanTestSupport.java ++++ b/tests/test-sessions/test-infinispan-sessions/src/test/java/org/eclipse/jetty/server/session/InfinispanTestSupport.java +@@ -1,6 +1,6 @@ + // + // ======================================================================== +-// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. ++// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others. + // ------------------------------------------------------------------------ + // All rights reserved. This program and the accompanying materials + // are made available under the terms of the Eclipse Public License v1.0 +@@ -16,38 +16,44 @@ + // ======================================================================== + // + +- + package org.eclipse.jetty.server.session; + +-import static org.junit.jupiter.api.Assertions.assertEquals; +-import static org.junit.jupiter.api.Assertions.assertTrue; +- + import java.io.File; ++import java.lang.annotation.ElementType; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.util.Properties; + ++import org.eclipse.jetty.toolchain.test.FS; + import org.eclipse.jetty.util.IO; ++import org.hibernate.search.cfg.Environment; ++import org.hibernate.search.cfg.SearchMapping; + import org.infinispan.Cache; +-import org.infinispan.configuration.cache.Configuration; + import org.infinispan.configuration.cache.ConfigurationBuilder; ++import org.infinispan.configuration.cache.ConfigurationChildBuilder; ++import org.infinispan.configuration.cache.Index; + import org.infinispan.configuration.global.GlobalConfigurationBuilder; + import org.infinispan.manager.DefaultCacheManager; + import org.infinispan.manager.EmbeddedCacheManager; + ++import static org.junit.jupiter.api.Assertions.assertEquals; ++import static org.junit.jupiter.api.Assertions.assertTrue; ++ + /** + * InfinispanTestSupport +- * +- * + */ + public class InfinispanTestSupport + { +- public static final String DEFAULT_CACHE_NAME = "session_test_cache"; +- public Cache _cache; +- ++ public static final String DEFAULT_CACHE_NAME = "session_test_cache"; ++ public Cache _cache; ++ + public ConfigurationBuilder _builder; +- private File _tmpdir; ++ private File _tmpdir; + private boolean _useFileStore; ++ private boolean _serializeSessionData; + private String _name; +- public static EmbeddedCacheManager _manager; +- ++ public static EmbeddedCacheManager _manager; ++ + static + { + try +@@ -59,53 +65,84 @@ public class InfinispanTestSupport + e.printStackTrace(); + } + } +- +- +- +- +- public InfinispanTestSupport () ++ ++ public InfinispanTestSupport() + { +- this (null); ++ this(null); + } +- ++ + public InfinispanTestSupport(String cacheName) +- { ++ { + if (cacheName == null) +- cacheName = DEFAULT_CACHE_NAME+System.currentTimeMillis(); +- ++ cacheName = DEFAULT_CACHE_NAME + System.currentTimeMillis(); ++ + _name = cacheName; + _builder = new ConfigurationBuilder(); + } +- +- public void setUseFileStore (boolean useFileStore) ++ ++ public void setUseFileStore(boolean useFileStore) + { + _useFileStore = useFileStore; + } +- +- public Cache getCache () ++ ++ public void setSerializeSessionData(boolean serializeSessionData) + { +- return _cache; ++ _serializeSessionData = serializeSessionData; + } + +- public void setup () throws Exception ++ public Cache getCache() + { +- if (_useFileStore) +- { +- _tmpdir = File.createTempFile("infini", "span"); +- _tmpdir.delete(); +- _tmpdir.mkdir(); +- Configuration config = _builder.persistence().addSingleFileStore().location(_tmpdir.getAbsolutePath()).storeAsBinary().build(); +- _manager.defineConfiguration(_name, config); +- } +- else +- { +- _manager.defineConfiguration(_name, _builder.build()); +- } +- _cache = _manager.getCache(_name); ++ return _cache; + } + ++ public void setup(Path root) throws Exception ++ { ++ Path indexesDir = root.resolve("indexes"); ++ FS.ensureDirExists(indexesDir); ++ ++ SearchMapping mapping = new SearchMapping(); ++ mapping.entity(SessionData.class).indexed().providedId().property("expiry", ElementType.FIELD).field(); ++ Properties properties = new Properties(); ++ properties.put(Environment.MODEL_MAPPING, mapping); ++ properties.put("hibernate.search.default.indexBase", indexesDir.toString()); ++ ++ if (_useFileStore) ++ { ++ Path tmpDir = Files.createTempDirectory("infinispan"); ++ _tmpdir = tmpDir.toFile(); ++ ++ ConfigurationChildBuilder b = _builder.indexing() ++ .index(Index.ALL) ++ .addIndexedEntity(SessionData.class) ++ .withProperties(properties) ++ .persistence() ++ .addSingleFileStore() ++ .location(_tmpdir.getAbsolutePath()); ++ if (_serializeSessionData) ++ { ++ b = b.storeAsBinary().enable(); ++ } ++ ++ _manager.defineConfiguration(_name, b.build()); ++ } ++ else ++ { ++ ConfigurationChildBuilder b = _builder.indexing() ++ .withProperties(properties) ++ .index(Index.ALL) ++ .addIndexedEntity(SessionData.class); ++ ++ if (_serializeSessionData) ++ { ++ b = b.storeAsBinary().enable(); ++ } ++ ++ _manager.defineConfiguration(_name, b.build()); ++ } ++ _cache = _manager.getCache(_name); ++ } + +- public void teardown () throws Exception ++ public void teardown() throws Exception + { + _cache.clear(); + _manager.removeCache(_name); +@@ -117,39 +154,41 @@ public class InfinispanTestSupport + } + } + } +- +- ++ + @SuppressWarnings("unchecked") +- public void createSession (SessionData data) +- throws Exception ++ public void createSession(SessionData data) ++ throws Exception + { +- _cache.put(data.getContextPath()+"_"+data.getVhost()+"_"+data.getId(), data); ++ _cache.put(data.getContextPath() + "_" + data.getVhost() + "_" + data.getId(), data); + } + +- +- public void createUnreadableSession (SessionData data) ++ public void createUnreadableSession(SessionData data) + { +- ++ + } +- +- +- public boolean checkSessionExists (SessionData data) +- throws Exception ++ ++ public boolean checkSessionExists(SessionData data) ++ throws Exception + { +- return (_cache.get(data.getContextPath()+"_"+data.getVhost()+"_"+data.getId()) != null); ++ return (_cache.get(data.getContextPath() + "_" + data.getVhost() + "_" + data.getId()) != null); + } +- +- +- public boolean checkSessionPersisted (SessionData data) +- throws Exception ++ ++ public boolean checkSessionPersisted(SessionData data) ++ throws Exception + { +- Object obj = _cache.get(data.getContextPath()+"_"+data.getVhost()+"_"+data.getId()); ++ ++ //evicts the object from memory. Forces the cache to fetch the data from file ++ if (_useFileStore) ++ { ++ _cache.evict(data.getContextPath() + "_" + data.getVhost() + "_" + data.getId()); ++ } ++ ++ Object obj = _cache.get(data.getContextPath() + "_" + data.getVhost() + "_" + data.getId()); + if (obj == null) + return false; +- ++ + SessionData saved = (SessionData)obj; +- +- ++ + //turn an Entity into a Session + assertEquals(data.getId(), saved.getId()); + assertEquals(data.getContextPath(), saved.getContextPath()); +@@ -168,11 +207,11 @@ public class InfinispanTestSupport + //same keys + assertTrue(data.getKeys().equals(saved.getKeys())); + //same values +- for (String name:data.getKeys()) ++ for (String name : data.getKeys()) + { + assertTrue(data.getAttribute(name).equals(saved.getAttribute(name))); + } +- ++ + return true; + } + } +diff --git a/tests/test-webapps/test-jetty-webapp/src/test/java/org/eclipse/jetty/TestServer.java b/tests/test-webapps/test-jetty-webapp/src/test/java/org/eclipse/jetty/TestServer.java +index a7af064..c1c5dc5 100644 +--- a/tests/test-webapps/test-jetty-webapp/src/test/java/org/eclipse/jetty/TestServer.java ++++ b/tests/test-webapps/test-jetty-webapp/src/test/java/org/eclipse/jetty/TestServer.java +@@ -1,6 +1,6 @@ + // + // ======================================================================== +-// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. ++// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others. + // ------------------------------------------------------------------------ + // All rights reserved. This program and the accompanying materials + // are made available under the terms of the Eclipse Public License v1.0 +@@ -18,13 +18,11 @@ + + package org.eclipse.jetty; + +-import java.io.File; + import java.io.IOException; + import java.lang.management.ManagementFactory; + import java.nio.file.FileSystems; + import java.nio.file.Files; + import java.nio.file.Path; +- + import javax.servlet.ServletException; + import javax.servlet.http.HttpServletRequest; + import javax.servlet.http.HttpServletResponse; +@@ -65,11 +63,11 @@ public class TestServer + ((StdErrLog)Log.getLog()).setSource(false); + + // TODO don't depend on this file structure +- Path jetty_root = FileSystems.getDefault().getPath(".").toAbsolutePath().normalize(); +- if (!Files.exists(jetty_root.resolve("VERSION.txt"))) +- jetty_root = FileSystems.getDefault().getPath("../../..").toAbsolutePath().normalize(); +- if (!Files.exists(jetty_root.resolve("VERSION.txt"))) +- throw new IllegalArgumentException(jetty_root.toString()); ++ Path jettyRoot = FileSystems.getDefault().getPath(".").toAbsolutePath().normalize(); ++ if (!Files.exists(jettyRoot.resolve("VERSION.txt"))) ++ jettyRoot = FileSystems.getDefault().getPath("../../..").toAbsolutePath().normalize(); ++ if (!Files.exists(jettyRoot.resolve("VERSION.txt"))) ++ throw new IllegalArgumentException(jettyRoot.toString()); + + // Setup Threadpool + QueuedThreadPool threadPool = new QueuedThreadPool(); +@@ -80,10 +78,9 @@ public class TestServer + server.manage(threadPool); + + // Setup JMX +- MBeanContainer mbContainer=new MBeanContainer(ManagementFactory.getPlatformMBeanServer()); ++ MBeanContainer mbContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer()); + server.addBean(mbContainer); + server.addBean(Log.getLog()); +- + + // Common HTTP configuration + HttpConfiguration config = new HttpConfiguration(); +@@ -92,21 +89,19 @@ public class TestServer + config.addCustomizer(new SecureRequestCustomizer()); + config.setSendDateHeader(true); + config.setSendServerVersion(true); +- +- ++ + // Http Connector + HttpConnectionFactory http = new HttpConnectionFactory(config); +- ServerConnector httpConnector = new ServerConnector(server,http); ++ ServerConnector httpConnector = new ServerConnector(server, http); + httpConnector.setPort(8080); + httpConnector.setIdleTimeout(30000); + server.addConnector(httpConnector); + +- + // Handlers + HandlerCollection handlers = new HandlerCollection(); + ContextHandlerCollection contexts = new ContextHandlerCollection(); + handlers.setHandlers(new Handler[] +- { contexts, new DefaultHandler() }); ++ {contexts, new DefaultHandler()}); + + // Add restart handler to test the ability to save sessions and restart + RestartHandler restart = new RestartHandler(); +@@ -114,15 +109,14 @@ public class TestServer + + server.setHandler(restart); + +- + // Setup context + HashLoginService login = new HashLoginService(); + login.setName("Test Realm"); +- login.setConfig(jetty_root.resolve("tests/test-webapps/test-jetty-webapp/src/main/config/demo-base/etc/realm.properties").toString()); ++ login.setConfig(jettyRoot.resolve("tests/test-webapps/test-jetty-webapp/src/main/config/demo-base/etc/realm.properties").toString()); + server.addBean(login); + +- File log=File.createTempFile("jetty-yyyy_mm_dd", "log"); +- CustomRequestLog requestLog = new CustomRequestLog(log.toString()); ++ Path logPath = Files.createTempFile("jetty-yyyy_mm_dd", "log"); ++ CustomRequestLog requestLog = new CustomRequestLog(logPath.toString()); + server.setRequestLog(requestLog); + + server.setStopAtShutdown(true); +@@ -130,23 +124,19 @@ public class TestServer + WebAppContext webapp = new WebAppContext(); + webapp.setContextPath("/test"); + webapp.setParentLoaderPriority(true); +- webapp.setResourceBase(jetty_root.resolve("tests/test-webapps/test-jetty-webapp/src/main/webapp").toString()); +- webapp.setAttribute("testAttribute","testValue"); +- File sessiondir=File.createTempFile("sessions",null); +- if (sessiondir.exists()) +- sessiondir.delete(); +- sessiondir.mkdir(); +- sessiondir.deleteOnExit(); ++ webapp.setResourceBase(jettyRoot.resolve("tests/test-webapps/test-jetty-webapp/src/main/webapp").toString()); ++ webapp.setAttribute("testAttribute", "testValue"); ++ Path sessionDir = Files.createTempDirectory("sessions"); + DefaultSessionCache ss = new DefaultSessionCache(webapp.getSessionHandler()); + FileSessionDataStore sds = new FileSessionDataStore(); + ss.setSessionDataStore(sds); +- sds.setStoreDir(sessiondir); ++ sds.setStoreDir(sessionDir.toFile()); + webapp.getSessionHandler().setSessionCache(ss); + + contexts.addHandler(webapp); + + ContextHandler srcroot = new ContextHandler(); +- srcroot.setResourceBase(jetty_root.resolve("tests/test-webapps/test-jetty-webapp/src").toString()); ++ srcroot.setResourceBase(jettyRoot.resolve("tests/test-webapps/test-jetty-webapp/src").toString()); + srcroot.setHandler(new ResourceHandler()); + srcroot.setContextPath("/src"); + contexts.addHandler(srcroot); +@@ -158,17 +148,17 @@ public class TestServer + + private static class RestartHandler extends HandlerWrapper + { +- /* ------------------------------------------------------------ */ ++ + /** + * @see org.eclipse.jetty.server.handler.HandlerWrapper#handle(java.lang.String, org.eclipse.jetty.server.Request, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + */ + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { +- super.handle(target,baseRequest,request,response); ++ super.handle(target, baseRequest, request, response); + if (Boolean.valueOf(request.getParameter("restart"))) + { +- final Server server=getServer(); ++ final Server server = getServer(); + + new Thread() + { +@@ -182,7 +172,7 @@ public class TestServer + Thread.sleep(100); + server.start(); + } +- catch(Exception e) ++ catch (Exception e) + { + LOG.warn(e); + } diff --git a/CVE-2020-27223-pre-1.patch b/CVE-2020-27223-pre-1.patch deleted file mode 100644 index 597b053..0000000 --- a/CVE-2020-27223-pre-1.patch +++ /dev/null @@ -1,545 +0,0 @@ -From 8011c48685e78850a88e7cc06650f678a375037d Mon Sep 17 00:00:00 2001 -From: Greg Wilkins -Date: Thu, 28 Feb 2019 18:37:02 +1100 -Subject: [PATCH 1/4] Issue #3404 Updated QCSV Double usage - -Signed-off-by: Greg Wilkins ---- - .../eclipse/jetty/http/QuotedQualityCSV.java | 58 ++++++++++--------- - 1 file changed, 30 insertions(+), 28 deletions(-) - -diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java b/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java -index 61cee88bfd5..c7047c57a4b 100644 ---- a/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java -+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java -@@ -25,6 +25,8 @@ - import java.util.List; - import java.util.function.Function; - -+import org.eclipse.jetty.util.log.Log; -+ - /* ------------------------------------------------------------ */ - /** - * Implements a quoted comma separated list of quality values -@@ -37,21 +39,16 @@ - */ - public class QuotedQualityCSV extends QuotedCSV implements Iterable - { -- private final static Double ZERO=new Double(0.0); -- private final static Double ONE=new Double(1.0); -- -+ private final static Double ZERO = 0.0D; -+ private final static Double ONE = 1.0D; - - /** -- * Function to apply a most specific MIME encoding secondary ordering -+ * Lambda to apply a most specific MIME encoding secondary ordering - */ -- public static Function MOST_SPECIFIC = new Function() -+ public static Function MOST_SPECIFIC = s -> - { -- @Override -- public Integer apply(String s) -- { -- String[] elements = s.split("/"); -- return 1000000*elements.length+1000*elements[0].length()+elements[elements.length-1].length(); -- } -+ String[] elements = s.split("/"); -+ return 1000000*elements.length+1000*elements[0].length()+elements[elements.length-1].length(); - }; - - private final List _quality = new ArrayList<>(); -@@ -74,7 +71,8 @@ public QuotedQualityCSV() - */ - public QuotedQualityCSV(String[] preferredOrder) - { -- this((s) -> { -+ this((s) -> -+ { - for (int i=0;i secondaryOrdering) - protected void parsedValue(StringBuffer buffer) - { - super.parsedValue(buffer); -+ -+ // Assume a quality of ONE - _quality.add(ONE); - } - -@@ -108,30 +108,32 @@ protected void parsedValue(StringBuffer buffer) - @Override - protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue) - { -- if (paramName<0) -+ if (paramName < 0) - { -- if (buffer.charAt(buffer.length()-1)==';') -- buffer.setLength(buffer.length()-1); -+ if (buffer.charAt(buffer.length() - 1) == ';') -+ buffer.setLength(buffer.length() - 1); - } -- else if (paramValue>=0 && -- buffer.charAt(paramName)=='q' && paramValue>paramName && -- buffer.length()>=paramName && buffer.charAt(paramName+1)=='=') -+ else if (paramValue >= 0 && -+ buffer.charAt(paramName) == 'q' && paramValue > paramName && -+ buffer.length() >= paramName && buffer.charAt(paramName + 1) == '=') - { - Double q; - try - { -- q=(_keepQuotes && buffer.charAt(paramValue)=='"') -- ?new Double(buffer.substring(paramValue+1,buffer.length()-1)) -- :new Double(buffer.substring(paramValue)); -+ q = (_keepQuotes && buffer.charAt(paramValue) == '"') -+ ? Double.valueOf(buffer.substring(paramValue + 1, buffer.length() - 1)) -+ : Double.valueOf(buffer.substring(paramValue)); - } -- catch(Exception e) -+ catch (Exception e) - { -- q=ZERO; -- } -- buffer.setLength(Math.max(0,paramName-1)); -- -- if (!ONE.equals(q)) -- _quality.set(_quality.size()-1,q); -+ Log.getLogger(QuotedQualityCSV.class).ignore(e); -+ q = ZERO; -+ } -+ buffer.setLength(Math.max(0, paramName - 1)); -+ -+ if (!ONE.equals(q)) -+ // replace assumed quality -+ _quality.set(_quality.size() - 1, q); - } - } - - -From ca8a10a9d5a50e7d814c65134144f776bff1c07a Mon Sep 17 00:00:00 2001 -From: Greg Wilkins -Date: Fri, 1 Mar 2019 10:17:02 +1100 -Subject: [PATCH 2/4] Issue #3404 Cleanup QCSV mime ordering - -Signed-off-by: Greg Wilkins ---- - .../org/eclipse/jetty/http/HttpFields.java | 16 ++++++++- - .../eclipse/jetty/http/QuotedQualityCSV.java | 35 ++++++++++--------- - .../jetty/http/QuotedQualityCSVTest.java | 2 +- - .../jetty/server/handler/ErrorHandler.java | 3 +- - 4 files changed, 37 insertions(+), 19 deletions(-) - -diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java -index 14c4335eb5a..5a8b53013a7 100644 ---- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java -+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java -@@ -31,6 +31,7 @@ - import java.util.NoSuchElementException; - import java.util.Set; - import java.util.StringTokenizer; -+import java.util.function.Function; - import java.util.stream.Stream; - import java.util.stream.StreamSupport; - -@@ -437,6 +438,19 @@ protected String addCSV(QuotedCSV existing,String... values) - * @param header The header - */ - public List getQualityCSV(HttpHeader header) -+ { -+ return getQualityCSV(header,null); -+ } -+ -+ /** -+ * Get multiple field values of the same name, split and -+ * sorted as a {@link QuotedQualityCSV} -+ * -+ * @param header The header -+ * @param secondaryOrdering Function to apply an ordering other than specified by quality -+ * @return List the values in quality order with the q param and OWS stripped -+ */ -+ public List getQualityCSV(HttpHeader header, Function secondaryOrdering) - { - QuotedQualityCSV values = null; - for (HttpField f : this) -@@ -444,7 +458,7 @@ protected String addCSV(QuotedCSV existing,String... values) - if (f.getHeader()==header) - { - if (values==null) -- values = new QuotedQualityCSV(); -+ values = new QuotedQualityCSV(secondaryOrdering); - values.addValue(f.getValue()); - } - } -diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java b/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java -index c7047c57a4b..498d4e01491 100644 ---- a/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java -+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java -@@ -38,17 +38,20 @@ - * @see "https://tools.ietf.org/html/rfc7231#section-5.3.1" - */ - public class QuotedQualityCSV extends QuotedCSV implements Iterable --{ -- private final static Double ZERO = 0.0D; -- private final static Double ONE = 1.0D; -- -+{ - /** -- * Lambda to apply a most specific MIME encoding secondary ordering -+ * Lambda to apply a most specific MIME encoding secondary ordering. -+ * @see "https://tools.ietf.org/html/rfc7231#section-5.3.2" - */ -- public static Function MOST_SPECIFIC = s -> -+ public static Function MOST_SPECIFIC_MIME_ORDERING = s -> - { -- String[] elements = s.split("/"); -- return 1000000*elements.length+1000*elements[0].length()+elements[elements.length-1].length(); -+ if ("*/*".equals(s)) -+ return 0; -+ if (s.endsWith("/*")) -+ return 1; -+ if (s.indexOf(';')<0) -+ return 2; -+ return 3; - }; - - private final List _quality = new ArrayList<>(); -@@ -61,7 +64,7 @@ - */ - public QuotedQualityCSV() - { -- this((s) -> 0); -+ this((Function)null); - } - - /* ------------------------------------------------------------ */ -@@ -91,7 +94,7 @@ public QuotedQualityCSV(String[] preferredOrder) - */ - public QuotedQualityCSV(Function secondaryOrdering) - { -- this._secondaryOrdering = secondaryOrdering; -+ this._secondaryOrdering = secondaryOrdering == null ? s->0 : secondaryOrdering; - } - - /* ------------------------------------------------------------ */ -@@ -101,7 +104,7 @@ protected void parsedValue(StringBuffer buffer) - super.parsedValue(buffer); - - // Assume a quality of ONE -- _quality.add(ONE); -+ _quality.add(1.0D); - } - - /* ------------------------------------------------------------ */ -@@ -127,11 +130,11 @@ else if (paramValue >= 0 && - catch (Exception e) - { - Log.getLogger(QuotedQualityCSV.class).ignore(e); -- q = ZERO; -+ q = 0.0D; - } - buffer.setLength(Math.max(0, paramName - 1)); - -- if (!ONE.equals(q)) -+ if (!((Double)1.0D).equals(q)) - // replace assumed quality - _quality.set(_quality.size() - 1, q); - } -@@ -157,7 +160,7 @@ protected void sort() - { - _sorted=true; - -- Double last = ZERO; -+ Double last = 0.0D; - int lastSecondaryOrder = Integer.MIN_VALUE; - - for (int i = _values.size(); i-- > 0;) -@@ -172,7 +175,7 @@ protected void sort() - _values.set(i + 1, v); - _quality.set(i, _quality.get(i + 1)); - _quality.set(i + 1, q); -- last = ZERO; -+ last = 0.0D; - lastSecondaryOrder=0; - i = _values.size(); - continue; -@@ -183,7 +186,7 @@ protected void sort() - } - - int last_element=_quality.size(); -- while(last_element>0 && _quality.get(--last_element).equals(ZERO)) -+ while(last_element>0 && _quality.get(--last_element).equals(0.0D)) - { - _quality.remove(last_element); - _values.remove(last_element); -diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/QuotedQualityCSVTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/QuotedQualityCSVTest.java -index ba0db4a8972..f03657ba3e5 100644 ---- a/jetty-http/src/test/java/org/eclipse/jetty/http/QuotedQualityCSVTest.java -+++ b/jetty-http/src/test/java/org/eclipse/jetty/http/QuotedQualityCSVTest.java -@@ -61,7 +61,7 @@ public void test7231_5_3_2_example3() - @Test - public void test7231_5_3_2_example3_most_specific() - { -- QuotedQualityCSV values = new QuotedQualityCSV(QuotedQualityCSV.MOST_SPECIFIC); -+ QuotedQualityCSV values = new QuotedQualityCSV(QuotedQualityCSV.MOST_SPECIFIC_MIME_ORDERING); - values.addValue("text/*, text/plain, text/plain;format=flowed, */*"); - - assertThat(values,Matchers.contains("text/plain;format=flowed","text/plain","text/*","*/*")); -diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java -index 84088146cd3..2820dca3a0c 100644 ---- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java -+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java -@@ -38,6 +38,7 @@ - import org.eclipse.jetty.http.HttpMethod; - import org.eclipse.jetty.http.HttpStatus; - import org.eclipse.jetty.http.MimeTypes; -+import org.eclipse.jetty.http.QuotedQualityCSV; - import org.eclipse.jetty.server.Dispatcher; - import org.eclipse.jetty.server.Request; - import org.eclipse.jetty.server.Server; -@@ -159,7 +160,7 @@ else if (old_error_page!=null && old_error_page.equals(error_page)) - protected void generateAcceptableResponse(Request baseRequest, HttpServletRequest request, HttpServletResponse response, int code, String message) - throws IOException - { -- List acceptable=baseRequest.getHttpFields().getQualityCSV(HttpHeader.ACCEPT); -+ List acceptable=baseRequest.getHttpFields().getQualityCSV(HttpHeader.ACCEPT, QuotedQualityCSV.MOST_SPECIFIC_MIME_ORDERING); - - if (acceptable.isEmpty() && !baseRequest.getHttpFields().contains(HttpHeader.ACCEPT)) - { - -From b925380ede948ab7d8757e95a0ce384b2441625b Mon Sep 17 00:00:00 2001 -From: Greg Wilkins -Date: Fri, 1 Mar 2019 10:20:10 +1100 -Subject: [PATCH 3/4] Issue #3404 Updated QCSV Double usage - -Signed-off-by: Greg Wilkins ---- - .../src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java b/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java -index 498d4e01491..a7407d34199 100644 ---- a/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java -+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java -@@ -134,7 +134,7 @@ else if (paramValue >= 0 && - } - buffer.setLength(Math.max(0, paramName - 1)); - -- if (!((Double)1.0D).equals(q)) -+ if (q!=1.0D) - // replace assumed quality - _quality.set(_quality.size() - 1, q); - } - -From 69f6b3b6164228485744918a28ab6a0d4c3facae Mon Sep 17 00:00:00 2001 -From: Greg Wilkins -Date: Tue, 5 Mar 2019 08:59:55 +1100 -Subject: [PATCH 4/4] Issue #3404 - -updates after review: - + use ToIntFunction - + reformat - -Signed-off-by: Greg Wilkins ---- - .../org/eclipse/jetty/http/HttpFields.java | 4 +- - .../eclipse/jetty/http/QuotedQualityCSV.java | 62 +++++++++++-------- - 2 files changed, 37 insertions(+), 29 deletions(-) - -diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java -index 5a8b53013a7..395b87bde35 100644 ---- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java -+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java -@@ -31,7 +31,7 @@ - import java.util.NoSuchElementException; - import java.util.Set; - import java.util.StringTokenizer; --import java.util.function.Function; -+import java.util.function.ToIntFunction; - import java.util.stream.Stream; - import java.util.stream.StreamSupport; - -@@ -450,7 +450,7 @@ protected String addCSV(QuotedCSV existing,String... values) - * @param secondaryOrdering Function to apply an ordering other than specified by quality - * @return List the values in quality order with the q param and OWS stripped - */ -- public List getQualityCSV(HttpHeader header, Function secondaryOrdering) -+ public List getQualityCSV(HttpHeader header, ToIntFunction secondaryOrdering) - { - QuotedQualityCSV values = null; - for (HttpField f : this) -diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java b/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java -index a7407d34199..d148d9e65e1 100644 ---- a/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java -+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java -@@ -18,21 +18,23 @@ - - package org.eclipse.jetty.http; - --import static java.lang.Integer.MIN_VALUE; -- - import java.util.ArrayList; - import java.util.Iterator; - import java.util.List; --import java.util.function.Function; -+import java.util.function.ToIntFunction; - - import org.eclipse.jetty.util.log.Log; - -+import static java.lang.Integer.MIN_VALUE; -+ - /* ------------------------------------------------------------ */ -+ - /** - * Implements a quoted comma separated list of quality values - * in accordance with RFC7230 and RFC7231. -- * Values are returned sorted in quality order, with OWS and the -+ * Values are returned sorted in quality order, with OWS and the - * quality parameters removed. -+ * - * @see "https://tools.ietf.org/html/rfc7230#section-3.2.6" - * @see "https://tools.ietf.org/html/rfc7230#section-7" - * @see "https://tools.ietf.org/html/rfc7231#section-5.3.1" -@@ -41,44 +43,48 @@ - { - /** - * Lambda to apply a most specific MIME encoding secondary ordering. -+ * - * @see "https://tools.ietf.org/html/rfc7231#section-5.3.2" - */ -- public static Function MOST_SPECIFIC_MIME_ORDERING = s -> -+ public static ToIntFunction MOST_SPECIFIC_MIME_ORDERING = s -> - { - if ("*/*".equals(s)) - return 0; - if (s.endsWith("/*")) - return 1; -- if (s.indexOf(';')<0) -+ if (s.indexOf(';') < 0) - return 2; - return 3; - }; -- -+ - private final List _quality = new ArrayList<>(); - private boolean _sorted = false; -- private final Function _secondaryOrdering; -- -+ private final ToIntFunction _secondaryOrdering; -+ - /* ------------------------------------------------------------ */ -+ - /** - * Sorts values with equal quality according to the length of the value String. - */ - public QuotedQualityCSV() - { -- this((Function)null); -+ this((ToIntFunction)null); - } - - /* ------------------------------------------------------------ */ -+ - /** - * Sorts values with equal quality according to given order. -+ * - * @param preferredOrder Array indicating the preferred order of known values - */ - public QuotedQualityCSV(String[] preferredOrder) - { - this((s) -> - { -- for (int i=0;i secondaryOrdering) -+ public QuotedQualityCSV(ToIntFunction secondaryOrdering) - { -- this._secondaryOrdering = secondaryOrdering == null ? s->0 : secondaryOrdering; -+ this._secondaryOrdering = secondaryOrdering == null ? s -> 0 : secondaryOrdering; - } -- -+ - /* ------------------------------------------------------------ */ - @Override - protected void parsedValue(StringBuffer buffer) -@@ -134,7 +142,7 @@ else if (paramValue >= 0 && - } - buffer.setLength(Math.max(0, paramName - 1)); - -- if (q!=1.0D) -+ if (q != 1.0D) - // replace assumed quality - _quality.set(_quality.size() - 1, q); - } -@@ -147,7 +155,7 @@ else if (paramValue >= 0 && - sort(); - return _values; - } -- -+ - @Override - public Iterator iterator() - { -@@ -158,35 +166,35 @@ else if (paramValue >= 0 && - - protected void sort() - { -- _sorted=true; -+ _sorted = true; - - Double last = 0.0D; - int lastSecondaryOrder = Integer.MIN_VALUE; - -- for (int i = _values.size(); i-- > 0;) -+ for (int i = _values.size(); i-- > 0; ) - { - String v = _values.get(i); - Double q = _quality.get(i); - -- int compare=last.compareTo(q); -- if (compare>0 || (compare==0 && _secondaryOrdering.apply(v) 0 || (compare == 0 && _secondaryOrdering.applyAsInt(v) < lastSecondaryOrder)) - { - _values.set(i, _values.get(i + 1)); - _values.set(i + 1, v); - _quality.set(i, _quality.get(i + 1)); - _quality.set(i + 1, q); - last = 0.0D; -- lastSecondaryOrder=0; -+ lastSecondaryOrder = 0; - i = _values.size(); - continue; - } - -- last=q; -- lastSecondaryOrder=_secondaryOrdering.apply(v); -+ last = q; -+ lastSecondaryOrder = _secondaryOrdering.applyAsInt(v); - } -- -- int last_element=_quality.size(); -- while(last_element>0 && _quality.get(--last_element).equals(0.0D)) -+ -+ int last_element = _quality.size(); -+ while (last_element > 0 && _quality.get(--last_element).equals(0.0D)) - { - _quality.remove(last_element); - _values.remove(last_element); diff --git a/CVE-2020-27223-pre-2.patch b/CVE-2020-27223-pre-2.patch deleted file mode 100644 index 989fd75..0000000 --- a/CVE-2020-27223-pre-2.patch +++ /dev/null @@ -1,692 +0,0 @@ -From 05072b34dce43064e87ad0d59065f14666c1f34e Mon Sep 17 00:00:00 2001 -From: Greg Wilkins -Date: Wed, 8 May 2019 13:28:16 +0200 -Subject: [PATCH] Issue #3630 Forwarded-Port - -Added support for the X-Forwarded-Port header. -Reimplemented header scanning using more efficient Trie and MethodHandles - -Signed-off-by: Greg Wilkins ---- - .../eclipse/jetty/http/HostPortHttpField.java | 13 + - .../org/eclipse/jetty/http/HttpHeader.java | 1 + - .../main/config/etc/jetty-http-forwarded.xml | 1 + - .../main/config/modules/http-forwarded.mod | 1 + - .../server/ForwardedRequestCustomizer.java | 274 +++++++++++++----- - .../ForwardedRequestCustomizerTest.java | 27 +- - .../java/org/eclipse/jetty/util/HostPort.java | 30 +- - .../org/eclipse/jetty/util/HostPortTest.java | 13 +- - 8 files changed, 269 insertions(+), 91 deletions(-) - -diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HostPortHttpField.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HostPortHttpField.java -index 215c353b4b9..dc386665339 100644 ---- a/jetty-http/src/main/java/org/eclipse/jetty/http/HostPortHttpField.java -+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HostPortHttpField.java -@@ -49,6 +49,19 @@ protected HostPortHttpField(HttpHeader header, String name, String authority) - } - } - -+ /* ------------------------------------------------------------ */ -+ public HostPortHttpField(String host, int port) -+ { -+ this(new HostPort(host, port)); -+ } -+ -+ /* ------------------------------------------------------------ */ -+ protected HostPortHttpField(HostPort hostport) -+ { -+ super(HttpHeader.HOST,HttpHeader.HOST.asString(),hostport.toString()); -+ _hostPort = hostport; -+ } -+ - /* ------------------------------------------------------------ */ - /** Get the host. - * @return the host -diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java -index f39c0f7df98..ac6bf2be8da 100644 ---- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java -+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java -@@ -82,6 +82,7 @@ - TE("TE"), - USER_AGENT("User-Agent"), - X_FORWARDED_FOR("X-Forwarded-For"), -+ X_FORWARDED_PORT("X-Forwarded-Port"), - X_FORWARDED_PROTO("X-Forwarded-Proto"), - X_FORWARDED_SERVER("X-Forwarded-Server"), - X_FORWARDED_HOST("X-Forwarded-Host"), -diff --git a/jetty-server/src/main/config/etc/jetty-http-forwarded.xml b/jetty-server/src/main/config/etc/jetty-http-forwarded.xml -index 50b80976a2a..648d6c6a94f 100644 ---- a/jetty-server/src/main/config/etc/jetty-http-forwarded.xml -+++ b/jetty-server/src/main/config/etc/jetty-http-forwarded.xml -@@ -11,6 +11,7 @@ - - - -+ - - - -diff --git a/jetty-server/src/main/config/modules/http-forwarded.mod b/jetty-server/src/main/config/modules/http-forwarded.mod -index 34e25642b2c..f67822065a4 100644 ---- a/jetty-server/src/main/config/modules/http-forwarded.mod -+++ b/jetty-server/src/main/config/modules/http-forwarded.mod -@@ -23,6 +23,7 @@ etc/jetty-http-forwarded.xml - # jetty.httpConfig.forwardedServerHeader=X-Forwarded-Server - # jetty.httpConfig.forwardedProtoHeader=X-Forwarded-Proto - # jetty.httpConfig.forwardedForHeader=X-Forwarded-For -+# jetty.httpConfig.forwardedPortHeader=X-Forwarded-Port - # jetty.httpConfig.forwardedHttpsHeader=X-Proxied-Https - # jetty.httpConfig.forwardedSslSessionIdHeader=Proxy-ssl-id - # jetty.httpConfig.forwardedCipherSuiteHeader=Proxy-auth-cert -diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ForwardedRequestCustomizer.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ForwardedRequestCustomizer.java -index f2ffe24fb85..1e64892cbe9 100644 ---- a/jetty-server/src/main/java/org/eclipse/jetty/server/ForwardedRequestCustomizer.java -+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ForwardedRequestCustomizer.java -@@ -18,6 +18,9 @@ - - package org.eclipse.jetty.server; - -+import java.lang.invoke.MethodHandle; -+import java.lang.invoke.MethodHandles; -+import java.lang.invoke.MethodType; - import java.net.InetSocketAddress; - - import javax.servlet.ServletRequest; -@@ -29,11 +32,15 @@ - import org.eclipse.jetty.http.HttpScheme; - import org.eclipse.jetty.http.QuotedCSV; - import org.eclipse.jetty.server.HttpConfiguration.Customizer; -+import org.eclipse.jetty.util.ArrayTrie; - import org.eclipse.jetty.util.HostPort; - import org.eclipse.jetty.util.StringUtil; -+import org.eclipse.jetty.util.Trie; - import org.eclipse.jetty.util.log.Log; - import org.eclipse.jetty.util.log.Logger; - -+import static java.lang.invoke.MethodType.methodType; -+ - - /* ------------------------------------------------------------ */ - /** Customize Requests for Proxy Forwarding. -@@ -63,14 +70,21 @@ - private String _forwardedHeader = HttpHeader.FORWARDED.toString(); - private String _forwardedHostHeader = HttpHeader.X_FORWARDED_HOST.toString(); - private String _forwardedServerHeader = HttpHeader.X_FORWARDED_SERVER.toString(); -- private String _forwardedForHeader = HttpHeader.X_FORWARDED_FOR.toString(); - private String _forwardedProtoHeader = HttpHeader.X_FORWARDED_PROTO.toString(); -+ private String _forwardedForHeader = HttpHeader.X_FORWARDED_FOR.toString(); -+ private String _forwardedPortHeader = HttpHeader.X_FORWARDED_PORT.toString(); - private String _forwardedHttpsHeader = "X-Proxied-Https"; - private String _forwardedCipherSuiteHeader = "Proxy-auth-cert"; - private String _forwardedSslSessionIdHeader = "Proxy-ssl-id"; - private boolean _proxyAsAuthority=false; - private boolean _sslIsSecure=true; -- -+ private Trie _handles; -+ -+ public ForwardedRequestCustomizer() -+ { -+ updateHandles(); -+ } -+ - /** - * @return true if the proxy address obtained via - * {@code X-Forwarded-Server} or RFC7239 "by" is used as -@@ -103,9 +117,9 @@ public void setForwardedOnly(boolean rfc7239only) - if (_forwardedHeader==null) - _forwardedHeader=HttpHeader.FORWARDED.toString(); - _forwardedHostHeader=null; -- _forwardedHostHeader=null; - _forwardedServerHeader=null; - _forwardedForHeader=null; -+ _forwardedPortHeader=null; - _forwardedProtoHeader=null; - _forwardedHttpsHeader=null; - } -@@ -117,11 +131,15 @@ public void setForwardedOnly(boolean rfc7239only) - _forwardedServerHeader = HttpHeader.X_FORWARDED_SERVER.toString(); - if (_forwardedForHeader==null) - _forwardedForHeader = HttpHeader.X_FORWARDED_FOR.toString(); -+ if (_forwardedPortHeader==null) -+ _forwardedPortHeader = HttpHeader.X_FORWARDED_PORT.toString(); - if (_forwardedProtoHeader==null) - _forwardedProtoHeader = HttpHeader.X_FORWARDED_PROTO.toString(); - if (_forwardedHttpsHeader==null) - _forwardedHttpsHeader = "X-Proxied-Https"; - } -+ -+ updateHandles(); - } - - public String getForcedHost() -@@ -138,6 +156,7 @@ public String getForcedHost() - public void setForcedHost(String hostAndPort) - { - _forcedHost = new HostPortHttpField(hostAndPort); -+ updateHandles(); - } - - /** -@@ -155,6 +174,7 @@ public String getForwardedHeader() - public void setForwardedHeader(String forwardedHeader) - { - _forwardedHeader = forwardedHeader; -+ updateHandles(); - } - - public String getForwardedHostHeader() -@@ -169,6 +189,7 @@ public String getForwardedHostHeader() - public void setForwardedHostHeader(String forwardedHostHeader) - { - _forwardedHostHeader = forwardedHostHeader; -+ updateHandles(); - } - - /** -@@ -186,6 +207,7 @@ public String getForwardedServerHeader() - public void setForwardedServerHeader(String forwardedServerHeader) - { - _forwardedServerHeader = forwardedServerHeader; -+ updateHandles(); - } - - /** -@@ -203,6 +225,22 @@ public String getForwardedForHeader() - public void setForwardedForHeader(String forwardedRemoteAddressHeader) - { - _forwardedForHeader = forwardedRemoteAddressHeader; -+ updateHandles(); -+ } -+ -+ public String getForwardedPortHeader() -+ { -+ return _forwardedHostHeader; -+ } -+ -+ /** -+ * @param forwardedPortHeader -+ * The header name for forwarded hosts (default {@code X-Forwarded-Port}) -+ */ -+ public void setForwardedPortHeader(String forwardedPortHeader) -+ { -+ _forwardedHostHeader = forwardedPortHeader; -+ updateHandles(); - } - - /** -@@ -224,6 +262,7 @@ public String getForwardedProtoHeader() - public void setForwardedProtoHeader(String forwardedProtoHeader) - { - _forwardedProtoHeader = forwardedProtoHeader; -+ updateHandles(); - } - - /** -@@ -241,6 +280,7 @@ public String getForwardedCipherSuiteHeader() - public void setForwardedCipherSuiteHeader(String forwardedCipherSuite) - { - _forwardedCipherSuiteHeader = forwardedCipherSuite; -+ updateHandles(); - } - - /** -@@ -258,6 +298,7 @@ public String getForwardedSslSessionIdHeader() - public void setForwardedSslSessionIdHeader(String forwardedSslSessionId) - { - _forwardedSslSessionIdHeader = forwardedSslSessionId; -+ updateHandles(); - } - - /** -@@ -274,6 +315,7 @@ public String getForwardedHttpsHeader() - public void setForwardedHttpsHeader(String forwardedHttpsHeader) - { - _forwardedHttpsHeader = forwardedHttpsHeader; -+ updateHandles(); - } - - /** -@@ -299,118 +341,84 @@ public void customize(Connector connector, HttpConfiguration config, Request req - { - HttpFields httpFields = request.getHttpFields(); - -- RFC7239 rfc7239 = null; -- String forwardedHost = null; -- String forwardedServer = null; -- HostPort forwardedFor = null; -- String forwardedProto = null; -- String forwardedHttps = null; -- - // Do a single pass through the header fields as it is a more efficient single iteration. -- for (HttpField field : httpFields) -+ Forwarded forwarded = new Forwarded(request, config); -+ try - { -- String name = field.getName(); -- -- if (getForwardedCipherSuiteHeader()!=null && getForwardedCipherSuiteHeader().equalsIgnoreCase(name)) -+ for (HttpField field : httpFields) - { -- request.setAttribute("javax.servlet.request.cipher_suite",field.getValue()); -- if (isSslIsSecure()) -- { -- request.setSecure(true); -- request.setScheme(config.getSecureScheme()); -- } -- } -- -- if (getForwardedSslSessionIdHeader()!=null && getForwardedSslSessionIdHeader().equalsIgnoreCase(name)) -- { -- request.setAttribute("javax.servlet.request.ssl_session_id", field.getValue()); -- if (isSslIsSecure()) -- { -- request.setSecure(true); -- request.setScheme(config.getSecureScheme()); -- } -- } -- -- if (forwardedHost==null && _forwardedHostHeader!=null && _forwardedHostHeader.equalsIgnoreCase(name)) -- forwardedHost = getLeftMost(field.getValue()); -- -- if (forwardedServer==null && _forwardedServerHeader!=null && _forwardedServerHeader.equalsIgnoreCase(name)) -- forwardedServer = getLeftMost(field.getValue()); -- -- if (forwardedFor==null && _forwardedForHeader!=null && _forwardedForHeader.equalsIgnoreCase(name)) -- forwardedFor = getRemoteAddr(field.getValue()); -- -- if (forwardedProto==null && _forwardedProtoHeader!=null && _forwardedProtoHeader.equalsIgnoreCase(name)) -- forwardedProto = getLeftMost(field.getValue()); -- -- if (forwardedHttps==null && _forwardedHttpsHeader!=null && _forwardedHttpsHeader.equalsIgnoreCase(name)) -- forwardedHttps = getLeftMost(field.getValue()); -- -- if (_forwardedHeader!=null && _forwardedHeader.equalsIgnoreCase(name)) -- { -- if (rfc7239==null) -- rfc7239= new RFC7239(); -- rfc7239.addValue(field.getValue()); -+ MethodHandle handle = _handles.get(field.getName()); -+ if (handle != null) -+ handle.invoke(forwarded, field); - } - } -- -- // Handle host header if if not available any RFC7230.by or X-ForwardedServer header -+ catch (Throwable e) -+ { -+ throw new RuntimeException(e); -+ } -+ -+ // Determine host - if (_forcedHost != null) - { - // Update host header - httpFields.put(_forcedHost); - request.setAuthority(_forcedHost.getHost(),_forcedHost.getPort()); - } -- else if (rfc7239!=null && rfc7239._host!=null) -+ else if (forwarded._rfc7239!=null && forwarded._rfc7239._host!=null) - { -- HostPortHttpField auth = rfc7239._host; -+ HostPortHttpField auth = forwarded._rfc7239._host; - httpFields.put(auth); - request.setAuthority(auth.getHost(),auth.getPort()); - } -- else if (forwardedHost != null) -+ else if (forwarded._forwardedHost != null) - { -- HostPortHttpField auth = new HostPortHttpField(forwardedHost); -+ HostPortHttpField auth = new HostPortHttpField(forwarded._forwardedHost); - httpFields.put(auth); -- request.setAuthority(auth.getHost(),auth.getPort()); -+ request.setAuthority(auth.getHost(), auth.getPort()); - } - else if (_proxyAsAuthority) - { -- if (rfc7239!=null && rfc7239._by!=null) -+ if (forwarded._rfc7239!=null && forwarded._rfc7239._by!=null) - { -- HostPortHttpField auth = rfc7239._by; -+ HostPortHttpField auth = forwarded._rfc7239._by; - httpFields.put(auth); - request.setAuthority(auth.getHost(),auth.getPort()); - } -- else if (forwardedServer != null) -+ else if (forwarded._forwardedServer != null) - { -- request.setAuthority(forwardedServer,request.getServerPort()); -+ request.setAuthority(forwarded._forwardedServer,request.getServerPort()); - } - } - - // handle remote end identifier -- if (rfc7239!=null && rfc7239._for!=null) -+ if (forwarded._rfc7239!=null && forwarded._rfc7239._for!=null) - { -- request.setRemoteAddr(InetSocketAddress.createUnresolved(rfc7239._for.getHost(),rfc7239._for.getPort())); -+ request.setRemoteAddr(InetSocketAddress.createUnresolved(forwarded._rfc7239._for.getHost(),forwarded._rfc7239._for.getPort())); - } -- else if (forwardedFor != null) -+ else if (forwarded._forwardedFor != null) - { -- request.setRemoteAddr(InetSocketAddress.createUnresolved(forwardedFor.getHost(), (forwardedFor.getPort() > 0) ? forwardedFor.getPort() : request.getRemotePort())); -+ int port = (forwarded._forwardedPort>0) -+ ? forwarded._forwardedPort -+ : (forwarded._forwardedFor.getPort() > 0) -+ ? forwarded._forwardedFor.getPort() -+ : request.getRemotePort(); -+ request.setRemoteAddr(InetSocketAddress.createUnresolved(forwarded._forwardedFor.getHost(), port)); - } - - // handle protocol identifier -- if (rfc7239!=null && rfc7239._proto!=null) -+ if (forwarded._rfc7239!=null && forwarded._rfc7239._proto!=null) - { -- request.setScheme(rfc7239._proto); -- if (rfc7239._proto.equals(config.getSecureScheme())) -+ request.setScheme(forwarded._rfc7239._proto); -+ if (forwarded._rfc7239._proto.equals(config.getSecureScheme())) - request.setSecure(true); - } -- else if (forwardedProto != null) -+ else if (forwarded._forwardedProto != null) - { -- request.setScheme(forwardedProto); -- if (forwardedProto.equals(config.getSecureScheme())) -+ request.setScheme(forwarded._forwardedProto); -+ if (forwarded._forwardedProto.equals(config.getSecureScheme())) - request.setSecure(true); - } -- else if (forwardedHttps !=null && ("on".equalsIgnoreCase(forwardedHttps)||"true".equalsIgnoreCase(forwardedHttps))) -+ else if (forwarded._forwardedHttps !=null && ("on".equalsIgnoreCase(forwarded._forwardedHttps)||"true".equalsIgnoreCase(forwarded._forwardedHttps))) - { - request.setScheme(HttpScheme.HTTPS.asString()); - if (HttpScheme.HTTPS.asString().equals(config.getSecureScheme())) -@@ -521,4 +529,122 @@ protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, - } - } - } -+ -+ private void updateHandles() -+ { -+ int size = 0; -+ MethodHandles.Lookup lookup = MethodHandles.lookup(); -+ MethodType type = methodType(Void.TYPE, HttpField.class); -+ -+ while(true) -+ { -+ try -+ { -+ size += 128; -+ _handles = new ArrayTrie<>(size); -+ -+ if (_forwardedCipherSuiteHeader != null && !_handles.put(_forwardedCipherSuiteHeader, lookup.findVirtual(Forwarded.class, "handleCipherSuite", type))) -+ continue; -+ if (_forwardedSslSessionIdHeader != null && !_handles.put(_forwardedSslSessionIdHeader, lookup.findVirtual(Forwarded.class, "handleSslSessionId", type))) -+ continue; -+ if (_forwardedHeader != null && !_handles.put(_forwardedHeader, lookup.findVirtual(Forwarded.class, "handleRFC7239", type))) -+ continue; -+ if (_forwardedForHeader != null && !_handles.put(_forwardedForHeader, lookup.findVirtual(Forwarded.class, "handleFor", type))) -+ continue; -+ if (_forwardedPortHeader != null && !_handles.put(_forwardedPortHeader, lookup.findVirtual(Forwarded.class, "handlePort", type))) -+ continue; -+ if (_forwardedHostHeader != null && !_handles.put(_forwardedHostHeader, lookup.findVirtual(Forwarded.class, "handleHost", type))) -+ continue; -+ if (_forwardedProtoHeader != null && !_handles.put(_forwardedProtoHeader, lookup.findVirtual(Forwarded.class, "handleProto", type))) -+ continue; -+ if (_forwardedHttpsHeader != null && !_handles.put(_forwardedHttpsHeader, lookup.findVirtual(Forwarded.class, "handleHttps", type))) -+ continue; -+ if (_forwardedServerHeader != null && !_handles.put(_forwardedServerHeader, lookup.findVirtual(Forwarded.class, "handleServer", type))) -+ continue; -+ break; -+ } -+ catch (NoSuchMethodException|IllegalAccessException e) -+ { -+ throw new IllegalStateException(e); -+ } -+ } -+ } -+ -+ private class Forwarded -+ { -+ HttpConfiguration _config; -+ Request _request; -+ -+ RFC7239 _rfc7239 = null; -+ String _forwardedHost = null; -+ String _forwardedServer = null; -+ String _forwardedProto = null; -+ HostPort _forwardedFor = null; -+ int _forwardedPort = -1; -+ String _forwardedHttps = null; -+ -+ public Forwarded(Request request, HttpConfiguration config) -+ { -+ _request = request; -+ _config = config; -+ } -+ -+ public void handleCipherSuite(HttpField field) -+ { -+ _request.setAttribute("javax.servlet.request.cipher_suite",field.getValue()); -+ if (isSslIsSecure()) -+ { -+ _request.setSecure(true); -+ _request.setScheme(_config.getSecureScheme()); -+ } -+ } -+ -+ public void handleSslSessionId(HttpField field) -+ { -+ _request.setAttribute("javax.servlet.request.ssl_session_id", field.getValue()); -+ if (isSslIsSecure()) -+ { -+ _request.setSecure(true); -+ _request.setScheme(_config.getSecureScheme()); -+ } -+ } -+ -+ public void handleHost(HttpField field) -+ { -+ _forwardedHost = getLeftMost(field.getValue()); -+ } -+ -+ public void handleServer(HttpField field) -+ { -+ _forwardedServer = getLeftMost(field.getValue()); -+ } -+ -+ public void handleProto(HttpField field) -+ { -+ _forwardedProto = getLeftMost(field.getValue()); -+ } -+ -+ public void handleFor(HttpField field) -+ { -+ _forwardedFor = getRemoteAddr(field.getValue()); -+ } -+ -+ public void handlePort(HttpField field) -+ { -+ _forwardedPort = field.getIntValue(); -+ } -+ public void handleHttps(HttpField field) -+ { -+ _forwardedHttps = getLeftMost(field.getValue()); -+ } -+ -+ public void handleRFC7239(HttpField field) -+ { -+ if (_rfc7239 ==null) -+ _rfc7239 = new RFC7239(); -+ _rfc7239.addValue(field.getValue()); -+ } -+ -+ -+ } - } -diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ForwardedRequestCustomizerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ForwardedRequestCustomizerTest.java -index 5d5467f73bd..0a1cecec0c2 100644 ---- a/jetty-server/src/test/java/org/eclipse/jetty/server/ForwardedRequestCustomizerTest.java -+++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ForwardedRequestCustomizerTest.java -@@ -18,11 +18,6 @@ - - package org.eclipse.jetty.server; - --import static org.junit.jupiter.api.Assertions.assertEquals; --import static org.junit.jupiter.api.Assertions.assertFalse; --import static org.hamcrest.MatcherAssert.assertThat; --import static org.junit.jupiter.api.Assertions.assertTrue; -- - import java.io.IOException; - import java.util.ArrayDeque; - import java.util.Deque; -@@ -41,6 +36,11 @@ - import org.junit.jupiter.api.BeforeEach; - import org.junit.jupiter.api.Test; - -+import static org.hamcrest.MatcherAssert.assertThat; -+import static org.junit.jupiter.api.Assertions.assertEquals; -+import static org.junit.jupiter.api.Assertions.assertFalse; -+import static org.junit.jupiter.api.Assertions.assertTrue; -+ - public class ForwardedRequestCustomizerTest - { - private Server _server; -@@ -249,6 +249,23 @@ public void testForIpv6WithPort() throws Exception - assertEquals("1111",_results.poll()); - } - -+ @Test -+ public void testForIpv6AndPort() throws Exception -+ { -+ String response=_connector.getResponse( -+ "GET / HTTP/1.1\n"+ -+ "Host: myhost\n"+ -+ "X-Forwarded-For: 1:2:3:4:5:6:7:8\n"+ -+ "X-Forwarded-Port: 2222\n"+ -+ "\n"); -+ assertThat(response, Matchers.containsString("200 OK")); -+ assertEquals("http",_results.poll()); -+ assertEquals("myhost",_results.poll()); -+ assertEquals("80",_results.poll()); -+ assertEquals("[1:2:3:4:5:6:7:8]",_results.poll()); -+ assertEquals("2222",_results.poll()); -+ } -+ - @Test - public void testLegacyProto() throws Exception - { -diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/HostPort.java b/jetty-util/src/main/java/org/eclipse/jetty/util/HostPort.java -index 1824dd799d1..8fde487ae19 100644 ---- a/jetty-util/src/main/java/org/eclipse/jetty/util/HostPort.java -+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/HostPort.java -@@ -32,6 +32,12 @@ - private final String _host; - private final int _port; - -+ public HostPort(String host, int port) throws IllegalArgumentException -+ { -+ _host = host; -+ _port = port; -+ } -+ - public HostPort(String authority) throws IllegalArgumentException - { - if (authority==null) -@@ -66,8 +72,16 @@ else if (authority.charAt(0)=='[') - int c = authority.lastIndexOf(':'); - if (c>=0) - { -- _host=authority.substring(0,c); -- _port=StringUtil.toInt(authority,c+1); -+ if (c!=authority.indexOf(':')) -+ { -+ _host="[" + authority + "]"; -+ _port=0; -+ } -+ else -+ { -+ _host = authority.substring(0, c); -+ _port = StringUtil.toInt(authority, c + 1); -+ } - } - else - { -@@ -93,7 +107,6 @@ else if (authority.charAt(0)=='[') - throw new IllegalArgumentException("Bad port"); - } - -- /* ------------------------------------------------------------ */ - /** Get the host. - * @return the host - */ -@@ -102,7 +115,6 @@ public String getHost() - return _host; - } - -- /* ------------------------------------------------------------ */ - /** Get the port. - * @return the port - */ -@@ -111,7 +123,6 @@ public int getPort() - return _port; - } - -- /* ------------------------------------------------------------ */ - /** Get the port. - * @param defaultPort, the default port to return if a port is not specified - * @return the port -@@ -121,7 +132,14 @@ public int getPort(int defaultPort) - return _port>0?_port:defaultPort; - } - -- /* ------------------------------------------------------------ */ -+ @Override -+ public String toString() -+ { -+ if (_port>0) -+ return normalizeHost(_host) + ":" + _port; -+ return _host; -+ } -+ - /** Normalize IPv6 address as per https://www.ietf.org/rfc/rfc2732.txt - * @param host A host name - * @return Host name surrounded by '[' and ']' as needed. -diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/HostPortTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/HostPortTest.java -index 705bbbf6fa6..a700262cb90 100644 ---- a/jetty-util/src/test/java/org/eclipse/jetty/util/HostPortTest.java -+++ b/jetty-util/src/test/java/org/eclipse/jetty/util/HostPortTest.java -@@ -18,17 +18,17 @@ - - package org.eclipse.jetty.util; - --import static org.hamcrest.MatcherAssert.assertThat; --import static org.hamcrest.Matchers.is; --import static org.junit.jupiter.api.Assertions.assertNull; --import static org.junit.jupiter.api.Assertions.assertThrows; -- - import java.util.stream.Stream; - - import org.junit.jupiter.params.ParameterizedTest; - import org.junit.jupiter.params.provider.Arguments; - import org.junit.jupiter.params.provider.MethodSource; - -+import static org.hamcrest.MatcherAssert.assertThat; -+import static org.hamcrest.Matchers.is; -+import static org.junit.jupiter.api.Assertions.assertNull; -+import static org.junit.jupiter.api.Assertions.assertThrows; -+ - public class HostPortTest - { - private static Stream validAuthorityProvider() -@@ -41,7 +41,8 @@ - Arguments.of("10.10.10.1", "10.10.10.1", null), - Arguments.of("10.10.10.1:80", "10.10.10.1", "80"), - Arguments.of("[0::0::0::1]", "[0::0::0::1]", null), -- Arguments.of("[0::0::0::1]:80", "[0::0::0::1]", "80") -+ Arguments.of("[0::0::0::1]:80", "[0::0::0::1]", "80"), -+ Arguments.of("0:1:2:3:4:5:6","[0:1:2:3:4:5:6]",null) - ); - } - diff --git a/CVE-2020-27223-pre-3.patch b/CVE-2020-27223-pre-3.patch deleted file mode 100644 index 356a049..0000000 --- a/CVE-2020-27223-pre-3.patch +++ /dev/null @@ -1,518 +0,0 @@ -From 9f3b0223ab7e3159c4794e102f6b5e06dfc8710d Mon Sep 17 00:00:00 2001 -From: Greg Wilkins -Date: Wed, 8 May 2019 14:00:58 +0200 -Subject: [PATCH] Issue #3630 Forwarded-Port - -reformatted code -Avoid updating handles unless configuration is changed. - -Signed-off-by: Greg Wilkins ---- - .../server/ForwardedRequestCustomizer.java | 216 ++++++++++-------- - 1 file changed, 117 insertions(+), 99 deletions(-) - -diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ForwardedRequestCustomizer.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ForwardedRequestCustomizer.java -index 1e64892cbe9..be3990794ed 100644 ---- a/jetty-server/src/main/java/org/eclipse/jetty/server/ForwardedRequestCustomizer.java -+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ForwardedRequestCustomizer.java -@@ -43,7 +43,9 @@ - - - /* ------------------------------------------------------------ */ --/** Customize Requests for Proxy Forwarding. -+ -+/** -+ * Customize Requests for Proxy Forwarding. - *

- * This customizer looks at at HTTP request for headers that indicate - * it has been forwarded by one or more proxies. Specifically handled are -@@ -59,7 +61,8 @@ - * so that the proxy is not seen as the other end point of the connection on which - * the request came

- *

Headers can also be defined so that forwarded SSL Session IDs and Cipher -- * suites may be customised

-+ * suites may be customised

-+ * - * @see Wikipedia: X-Forwarded-For - */ - public class ForwardedRequestCustomizer implements Customizer -@@ -76,9 +79,9 @@ - private String _forwardedHttpsHeader = "X-Proxied-Https"; - private String _forwardedCipherSuiteHeader = "Proxy-auth-cert"; - private String _forwardedSslSessionIdHeader = "Proxy-ssl-id"; -- private boolean _proxyAsAuthority=false; -- private boolean _sslIsSecure=true; -- private Trie _handles; -+ private boolean _proxyAsAuthority = false; -+ private boolean _sslIsSecure = true; -+ private Trie _handles; - - public ForwardedRequestCustomizer() - { -@@ -97,7 +100,7 @@ public boolean getProxyAsAuthority() - - /** - * @param proxyAsAuthority if true, use the proxy address obtained via -- * {@code X-Forwarded-Server} or RFC7239 "by" as the request authority. -+ * {@code X-Forwarded-Server} or RFC7239 "by" as the request authority. - */ - public void setProxyAsAuthority(boolean proxyAsAuthority) - { -@@ -114,49 +117,47 @@ public void setForwardedOnly(boolean rfc7239only) - { - if (rfc7239only) - { -- if (_forwardedHeader==null) -- _forwardedHeader=HttpHeader.FORWARDED.toString(); -- _forwardedHostHeader=null; -- _forwardedServerHeader=null; -- _forwardedForHeader=null; -- _forwardedPortHeader=null; -- _forwardedProtoHeader=null; -- _forwardedHttpsHeader=null; -+ if (_forwardedHeader == null) -+ _forwardedHeader = HttpHeader.FORWARDED.toString(); -+ _forwardedHostHeader = null; -+ _forwardedServerHeader = null; -+ _forwardedForHeader = null; -+ _forwardedPortHeader = null; -+ _forwardedProtoHeader = null; -+ _forwardedHttpsHeader = null; - } - else - { -- if (_forwardedHostHeader==null) -+ if (_forwardedHostHeader == null) - _forwardedHostHeader = HttpHeader.X_FORWARDED_HOST.toString(); -- if (_forwardedServerHeader==null) -+ if (_forwardedServerHeader == null) - _forwardedServerHeader = HttpHeader.X_FORWARDED_SERVER.toString(); -- if (_forwardedForHeader==null) -+ if (_forwardedForHeader == null) - _forwardedForHeader = HttpHeader.X_FORWARDED_FOR.toString(); -- if (_forwardedPortHeader==null) -+ if (_forwardedPortHeader == null) - _forwardedPortHeader = HttpHeader.X_FORWARDED_PORT.toString(); -- if (_forwardedProtoHeader==null) -+ if (_forwardedProtoHeader == null) - _forwardedProtoHeader = HttpHeader.X_FORWARDED_PROTO.toString(); -- if (_forwardedHttpsHeader==null) -+ if (_forwardedHttpsHeader == null) - _forwardedHttpsHeader = "X-Proxied-Https"; - } - - updateHandles(); - } -- -+ - public String getForcedHost() - { - return _forcedHost.getValue(); - } -- -+ - /** - * Set a forced valued for the host header to control what is returned by {@link ServletRequest#getServerName()} and {@link ServletRequest#getServerPort()}. - * -- * @param hostAndPort -- * The value of the host header to force. -+ * @param hostAndPort The value of the host header to force. - */ - public void setForcedHost(String hostAndPort) - { - _forcedHost = new HostPortHttpField(hostAndPort); -- updateHandles(); - } - - /** -@@ -168,13 +169,15 @@ public String getForwardedHeader() - } - - /** -- * @param forwardedHeader -- * The header name for RFC forwarded (default Forwarded) -+ * @param forwardedHeader The header name for RFC forwarded (default Forwarded) - */ - public void setForwardedHeader(String forwardedHeader) - { -- _forwardedHeader = forwardedHeader; -- updateHandles(); -+ if (_forwardedHeader == null || !_forwardedHeader.equals(forwardedHeader)) -+ { -+ _forwardedHeader = forwardedHeader; -+ updateHandles(); -+ } - } - - public String getForwardedHostHeader() -@@ -183,13 +186,15 @@ public String getForwardedHostHeader() - } - - /** -- * @param forwardedHostHeader -- * The header name for forwarded hosts (default {@code X-Forwarded-Host}) -+ * @param forwardedHostHeader The header name for forwarded hosts (default {@code X-Forwarded-Host}) - */ - public void setForwardedHostHeader(String forwardedHostHeader) - { -- _forwardedHostHeader = forwardedHostHeader; -- updateHandles(); -+ if (_forwardedHostHeader == null || !_forwardedHostHeader.equalsIgnoreCase(forwardedHostHeader)) -+ { -+ _forwardedHostHeader = forwardedHostHeader; -+ updateHandles(); -+ } - } - - /** -@@ -201,13 +206,15 @@ public String getForwardedServerHeader() - } - - /** -- * @param forwardedServerHeader -- * The header name for forwarded server (default {@code X-Forwarded-Server}) -+ * @param forwardedServerHeader The header name for forwarded server (default {@code X-Forwarded-Server}) - */ - public void setForwardedServerHeader(String forwardedServerHeader) - { -- _forwardedServerHeader = forwardedServerHeader; -- updateHandles(); -+ if (_forwardedServerHeader == null || !_forwardedServerHeader.equalsIgnoreCase(forwardedServerHeader)) -+ { -+ _forwardedServerHeader = forwardedServerHeader; -+ updateHandles(); -+ } - } - - /** -@@ -219,13 +226,15 @@ public String getForwardedForHeader() - } - - /** -- * @param forwardedRemoteAddressHeader -- * The header name for forwarded for (default {@code X-Forwarded-For}) -+ * @param forwardedRemoteAddressHeader The header name for forwarded for (default {@code X-Forwarded-For}) - */ - public void setForwardedForHeader(String forwardedRemoteAddressHeader) - { -- _forwardedForHeader = forwardedRemoteAddressHeader; -- updateHandles(); -+ if (_forwardedForHeader == null || !_forwardedForHeader.equalsIgnoreCase(forwardedRemoteAddressHeader)) -+ { -+ _forwardedForHeader = forwardedRemoteAddressHeader; -+ updateHandles(); -+ } - } - - public String getForwardedPortHeader() -@@ -234,13 +243,15 @@ public String getForwardedPortHeader() - } - - /** -- * @param forwardedPortHeader -- * The header name for forwarded hosts (default {@code X-Forwarded-Port}) -+ * @param forwardedPortHeader The header name for forwarded hosts (default {@code X-Forwarded-Port}) - */ - public void setForwardedPortHeader(String forwardedPortHeader) - { -- _forwardedHostHeader = forwardedPortHeader; -- updateHandles(); -+ if (_forwardedHostHeader == null || !_forwardedHostHeader.equalsIgnoreCase(forwardedPortHeader)) -+ { -+ _forwardedHostHeader = forwardedPortHeader; -+ updateHandles(); -+ } - } - - /** -@@ -256,13 +267,15 @@ public String getForwardedProtoHeader() - /** - * Set the forwardedProtoHeader. - * -- * @param forwardedProtoHeader -- * the forwardedProtoHeader to set (default {@code X-Forwarded-Proto}) -+ * @param forwardedProtoHeader the forwardedProtoHeader to set (default {@code X-Forwarded-Proto}) - */ - public void setForwardedProtoHeader(String forwardedProtoHeader) - { -- _forwardedProtoHeader = forwardedProtoHeader; -- updateHandles(); -+ if (_forwardedProtoHeader == null || !_forwardedProtoHeader.equalsIgnoreCase(forwardedProtoHeader)) -+ { -+ _forwardedProtoHeader = forwardedProtoHeader; -+ updateHandles(); -+ } - } - - /** -@@ -274,13 +287,15 @@ public String getForwardedCipherSuiteHeader() - } - - /** -- * @param forwardedCipherSuite -- * The header name holding a forwarded cipher suite (default {@code Proxy-auth-cert}) -+ * @param forwardedCipherSuiteHeader The header name holding a forwarded cipher suite (default {@code Proxy-auth-cert}) - */ -- public void setForwardedCipherSuiteHeader(String forwardedCipherSuite) -+ public void setForwardedCipherSuiteHeader(String forwardedCipherSuiteHeader) - { -- _forwardedCipherSuiteHeader = forwardedCipherSuite; -- updateHandles(); -+ if (_forwardedCipherSuiteHeader == null || !_forwardedCipherSuiteHeader.equalsIgnoreCase(forwardedCipherSuiteHeader)) -+ { -+ _forwardedCipherSuiteHeader = forwardedCipherSuiteHeader; -+ updateHandles(); -+ } - } - - /** -@@ -292,13 +307,15 @@ public String getForwardedSslSessionIdHeader() - } - - /** -- * @param forwardedSslSessionId -- * The header name holding a forwarded SSL Session ID (default {@code Proxy-ssl-id}) -+ * @param forwardedSslSessionIdHeader The header name holding a forwarded SSL Session ID (default {@code Proxy-ssl-id}) - */ -- public void setForwardedSslSessionIdHeader(String forwardedSslSessionId) -+ public void setForwardedSslSessionIdHeader(String forwardedSslSessionIdHeader) - { -- _forwardedSslSessionIdHeader = forwardedSslSessionId; -- updateHandles(); -+ if (_forwardedSslSessionIdHeader == null || !_forwardedSslSessionIdHeader.equalsIgnoreCase(forwardedSslSessionIdHeader)) -+ { -+ _forwardedSslSessionIdHeader = forwardedSslSessionIdHeader; -+ updateHandles(); -+ } - } - - /** -@@ -314,10 +331,13 @@ public String getForwardedHttpsHeader() - */ - public void setForwardedHttpsHeader(String forwardedHttpsHeader) - { -- _forwardedHttpsHeader = forwardedHttpsHeader; -- updateHandles(); -+ if (_forwardedHttpsHeader == null || !_forwardedHttpsHeader.equalsIgnoreCase(forwardedHttpsHeader)) -+ { -+ _forwardedHttpsHeader = forwardedHttpsHeader; -+ updateHandles(); -+ } - } -- -+ - /** - * @return true if the presence of a SSL session or certificate header is sufficient - * to indicate a secure request (default is true) -@@ -329,7 +349,7 @@ public boolean isSslIsSecure() - - /** - * @param sslIsSecure true if the presence of a SSL session or certificate header is sufficient -- * to indicate a secure request (default is true) -+ * to indicate a secure request (default is true) - */ - public void setSslIsSecure(boolean sslIsSecure) - { -@@ -362,13 +382,13 @@ public void customize(Connector connector, HttpConfiguration config, Request req - { - // Update host header - httpFields.put(_forcedHost); -- request.setAuthority(_forcedHost.getHost(),_forcedHost.getPort()); -+ request.setAuthority(_forcedHost.getHost(), _forcedHost.getPort()); - } -- else if (forwarded._rfc7239!=null && forwarded._rfc7239._host!=null) -+ else if (forwarded._rfc7239 != null && forwarded._rfc7239._host != null) - { - HostPortHttpField auth = forwarded._rfc7239._host; - httpFields.put(auth); -- request.setAuthority(auth.getHost(),auth.getPort()); -+ request.setAuthority(auth.getHost(), auth.getPort()); - } - else if (forwarded._forwardedHost != null) - { -@@ -378,26 +398,26 @@ else if (forwarded._forwardedHost != null) - } - else if (_proxyAsAuthority) - { -- if (forwarded._rfc7239!=null && forwarded._rfc7239._by!=null) -+ if (forwarded._rfc7239 != null && forwarded._rfc7239._by != null) - { - HostPortHttpField auth = forwarded._rfc7239._by; - httpFields.put(auth); -- request.setAuthority(auth.getHost(),auth.getPort()); -+ request.setAuthority(auth.getHost(), auth.getPort()); - } - else if (forwarded._forwardedServer != null) - { -- request.setAuthority(forwarded._forwardedServer,request.getServerPort()); -+ request.setAuthority(forwarded._forwardedServer, request.getServerPort()); - } - } - - // handle remote end identifier -- if (forwarded._rfc7239!=null && forwarded._rfc7239._for!=null) -+ if (forwarded._rfc7239 != null && forwarded._rfc7239._for != null) - { -- request.setRemoteAddr(InetSocketAddress.createUnresolved(forwarded._rfc7239._for.getHost(),forwarded._rfc7239._for.getPort())); -+ request.setRemoteAddr(InetSocketAddress.createUnresolved(forwarded._rfc7239._for.getHost(), forwarded._rfc7239._for.getPort())); - } - else if (forwarded._forwardedFor != null) - { -- int port = (forwarded._forwardedPort>0) -+ int port = (forwarded._forwardedPort > 0) - ? forwarded._forwardedPort - : (forwarded._forwardedFor.getPort() > 0) - ? forwarded._forwardedFor.getPort() -@@ -406,7 +426,7 @@ else if (forwarded._forwardedFor != null) - } - - // handle protocol identifier -- if (forwarded._rfc7239!=null && forwarded._rfc7239._proto!=null) -+ if (forwarded._rfc7239 != null && forwarded._rfc7239._proto != null) - { - request.setScheme(forwarded._rfc7239._proto); - if (forwarded._rfc7239._proto.equals(config.getSecureScheme())) -@@ -418,7 +438,7 @@ else if (forwarded._forwardedProto != null) - if (forwarded._forwardedProto.equals(config.getSecureScheme())) - request.setSecure(true); - } -- else if (forwarded._forwardedHttps !=null && ("on".equalsIgnoreCase(forwarded._forwardedHttps)||"true".equalsIgnoreCase(forwarded._forwardedHttps))) -+ else if (forwarded._forwardedHttps != null && ("on".equalsIgnoreCase(forwarded._forwardedHttps) || "true".equalsIgnoreCase(forwarded._forwardedHttps))) - { - request.setScheme(HttpScheme.HTTPS.asString()); - if (HttpScheme.HTTPS.asString().equals(config.getSecureScheme())) -@@ -441,7 +461,7 @@ protected String getLeftMost(String headerValue) - } - - // The left-most value is the farthest downstream client -- return headerValue.substring(0,commaIndex).trim(); -+ return headerValue.substring(0, commaIndex).trim(); - } - - protected HostPort getRemoteAddr(String headerValue) -@@ -463,11 +483,11 @@ protected HostPort getRemoteAddr(String headerValue) - return null; - } - } -- -+ - @Override - public String toString() - { -- return String.format("%s@%x",this.getClass().getSimpleName(),hashCode()); -+ return String.format("%s@%x", this.getClass().getSimpleName(), hashCode()); - } - - @Deprecated -@@ -475,12 +495,11 @@ public String getHostHeader() - { - return _forcedHost.getValue(); - } -- -+ - /** - * Set a forced valued for the host header to control what is returned by {@link ServletRequest#getServerName()} and {@link ServletRequest#getServerPort()}. - * -- * @param hostHeader -- * The value of the host header to force. -+ * @param hostHeader The value of the host header to force. - */ - @Deprecated - public void setHostHeader(String hostHeader) -@@ -494,7 +513,7 @@ public void setHostHeader(String hostHeader) - HostPortHttpField _for; - HostPortHttpField _host; - String _proto; -- -+ - private RFC7239() - { - super(false); -@@ -503,27 +522,27 @@ private RFC7239() - @Override - protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue) - { -- if (valueLength==0 && paramValue>paramName) -+ if (valueLength == 0 && paramValue > paramName) - { -- String name=StringUtil.asciiToLowerCase(buffer.substring(paramName,paramValue-1)); -- String value=buffer.substring(paramValue); -- switch(name) -+ String name = StringUtil.asciiToLowerCase(buffer.substring(paramName, paramValue - 1)); -+ String value = buffer.substring(paramValue); -+ switch (name) - { - case "by": -- if (_by==null && !value.startsWith("_") && !"unknown".equals(value)) -- _by=new HostPortHttpField(value); -+ if (_by == null && !value.startsWith("_") && !"unknown".equals(value)) -+ _by = new HostPortHttpField(value); - break; - case "for": -- if (_for==null && !value.startsWith("_") && !"unknown".equals(value)) -- _for=new HostPortHttpField(value); -+ if (_for == null && !value.startsWith("_") && !"unknown".equals(value)) -+ _for = new HostPortHttpField(value); - break; - case "host": -- if (_host==null) -- _host=new HostPortHttpField(value); -+ if (_host == null) -+ _host = new HostPortHttpField(value); - break; - case "proto": -- if (_proto==null) -- _proto=value; -+ if (_proto == null) -+ _proto = value; - break; - } - } -@@ -536,7 +555,7 @@ private void updateHandles() - MethodHandles.Lookup lookup = MethodHandles.lookup(); - MethodType type = methodType(Void.TYPE, HttpField.class); - -- while(true) -+ while (true) - { - try - { -@@ -563,7 +582,7 @@ private void updateHandles() - continue; - break; - } -- catch (NoSuchMethodException|IllegalAccessException e) -+ catch (NoSuchMethodException | IllegalAccessException e) - { - throw new IllegalStateException(e); - } -@@ -591,7 +610,7 @@ public Forwarded(Request request, HttpConfiguration config) - - public void handleCipherSuite(HttpField field) - { -- _request.setAttribute("javax.servlet.request.cipher_suite",field.getValue()); -+ _request.setAttribute("javax.servlet.request.cipher_suite", field.getValue()); - if (isSslIsSecure()) - { - _request.setSecure(true); -@@ -633,6 +652,7 @@ public void handlePort(HttpField field) - { - _forwardedPort = field.getIntValue(); - } -+ - public void handleHttps(HttpField field) - { - _forwardedHttps = getLeftMost(field.getValue()); -@@ -640,11 +660,9 @@ public void handleHttps(HttpField field) - - public void handleRFC7239(HttpField field) - { -- if (_rfc7239 ==null) -+ if (_rfc7239 == null) - _rfc7239 = new RFC7239(); - _rfc7239.addValue(field.getValue()); - } -- -- - } - } diff --git a/CVE-2020-27223-pre-4.patch b/CVE-2020-27223-pre-4.patch deleted file mode 100644 index b77d180..0000000 --- a/CVE-2020-27223-pre-4.patch +++ /dev/null @@ -1,1167 +0,0 @@ -From cec50b3d2cea7bdf6b2f00d45c8554139aebbf87 Mon Sep 17 00:00:00 2001 -From: Greg Wilkins -Date: Wed, 8 May 2019 21:07:22 +0200 -Subject: [PATCH] Issue #3630 Optimized ForwardedRequestCustomizer - -Signed-off-by: Greg Wilkins ---- - .../eclipse/jetty/http/HostPortHttpField.java | 17 +- - .../org/eclipse/jetty/http/QuotedCSV.java | 264 +--------------- - .../eclipse/jetty/http/QuotedCSVParser.java | 288 ++++++++++++++++++ - .../server/ForwardedRequestCustomizer.java | 249 +++++++-------- - .../ForwardedRequestCustomizerTest.java | 99 +++++- - 5 files changed, 507 insertions(+), 410 deletions(-) - create mode 100644 jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSVParser.java - -diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HostPortHttpField.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HostPortHttpField.java -index dc386665339..6c9f3b921e8 100644 ---- a/jetty-http/src/main/java/org/eclipse/jetty/http/HostPortHttpField.java -+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HostPortHttpField.java -@@ -22,9 +22,9 @@ - import org.eclipse.jetty.util.HostPort; - - -- --/* ------------------------------------------------------------ */ - /** -+ * A HttpField holding a preparsed Host and port number -+ * @see HostPort - */ - public class HostPortHttpField extends HttpField - { -@@ -35,7 +35,6 @@ public HostPortHttpField(String authority) - this(HttpHeader.HOST,HttpHeader.HOST.asString(),authority); - } - -- /* ------------------------------------------------------------ */ - protected HostPortHttpField(HttpHeader header, String name, String authority) - { - super(header,name,authority); -@@ -49,20 +48,17 @@ protected HostPortHttpField(HttpHeader header, String name, String authority) - } - } - -- /* ------------------------------------------------------------ */ - public HostPortHttpField(String host, int port) - { - this(new HostPort(host, port)); - } - -- /* ------------------------------------------------------------ */ -- protected HostPortHttpField(HostPort hostport) -+ public HostPortHttpField(HostPort hostport) - { - super(HttpHeader.HOST,HttpHeader.HOST.asString(),hostport.toString()); - _hostPort = hostport; - } - -- /* ------------------------------------------------------------ */ - /** Get the host. - * @return the host - */ -@@ -71,7 +67,6 @@ public String getHost() - return _hostPort.getHost(); - } - -- /* ------------------------------------------------------------ */ - /** Get the port. - * @return the port - */ -@@ -80,7 +75,6 @@ public int getPort() - return _hostPort.getPort(); - } - -- /* ------------------------------------------------------------ */ - /** Get the port. - * @param defaultPort The default port to return if no port set - * @return the port -@@ -89,4 +83,9 @@ public int getPort(int defaultPort) - { - return _hostPort.getPort(defaultPort); - } -+ -+ public HostPort getHostPort() -+ { -+ return _hostPort; -+ } - } -diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSV.java b/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSV.java -index 9ca7dbeec2c..8ebc2c36307 100644 ---- a/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSV.java -+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSV.java -@@ -22,8 +22,6 @@ - import java.util.Iterator; - import java.util.List; - --import org.eclipse.jetty.util.QuotedStringTokenizer; -- - /* ------------------------------------------------------------ */ - /** - * Implements a quoted comma separated list of values -@@ -32,226 +30,26 @@ - * @see "https://tools.ietf.org/html/rfc7230#section-3.2.6" - * @see "https://tools.ietf.org/html/rfc7230#section-7" - */ --public class QuotedCSV implements Iterable --{ -- private enum State { VALUE, PARAM_NAME, PARAM_VALUE}; -- -+public class QuotedCSV extends QuotedCSVParser implements Iterable -+{ - protected final List _values = new ArrayList<>(); -- protected final boolean _keepQuotes; -- -- /* ------------------------------------------------------------ */ -+ - public QuotedCSV(String... values) - { - this(true,values); - } - -- /* ------------------------------------------------------------ */ - public QuotedCSV(boolean keepQuotes,String... values) - { -- _keepQuotes=keepQuotes; -+ super(keepQuotes); - for (String v:values) - addValue(v); - } -- -- /* ------------------------------------------------------------ */ -- /** Add and parse a value string(s) -- * @param value A value that may contain one or more Quoted CSV items. -- */ -- public void addValue(String value) -- { -- if (value == null) -- return; -- -- StringBuffer buffer = new StringBuffer(); -- -- int l=value.length(); -- State state=State.VALUE; -- boolean quoted=false; -- boolean sloshed=false; -- int nws_length=0; -- int last_length=0; -- int value_length=-1; -- int param_name=-1; -- int param_value=-1; -- -- for (int i=0;i<=l;i++) -- { -- char c=i==l?0:value.charAt(i); -- -- // Handle quoting https://tools.ietf.org/html/rfc7230#section-3.2.6 -- if (quoted && c!=0) -- { -- if (sloshed) -- sloshed=false; -- else -- { -- switch(c) -- { -- case '\\': -- sloshed=true; -- if (!_keepQuotes) -- continue; -- break; -- case '"': -- quoted=false; -- if (!_keepQuotes) -- continue; -- break; -- } -- } -- -- buffer.append(c); -- nws_length=buffer.length(); -- continue; -- } -- -- // Handle common cases -- switch(c) -- { -- case ' ': -- case '\t': -- if (buffer.length()>last_length) // not leading OWS -- buffer.append(c); -- continue; -- -- case '"': -- quoted=true; -- if (_keepQuotes) -- { -- if (state==State.PARAM_VALUE && param_value<0) -- param_value=nws_length; -- buffer.append(c); -- } -- else if (state==State.PARAM_VALUE && param_value<0) -- param_value=nws_length; -- nws_length=buffer.length(); -- continue; -- -- case ';': -- buffer.setLength(nws_length); // trim following OWS -- if (state==State.VALUE) -- { -- parsedValue(buffer); -- value_length=buffer.length(); -- } -- else -- parsedParam(buffer,value_length,param_name,param_value); -- nws_length=buffer.length(); -- param_name=param_value=-1; -- buffer.append(c); -- last_length=++nws_length; -- state=State.PARAM_NAME; -- continue; -- -- case ',': -- case 0: -- if (nws_length>0) -- { -- buffer.setLength(nws_length); // trim following OWS -- switch(state) -- { -- case VALUE: -- parsedValue(buffer); -- value_length=buffer.length(); -- break; -- case PARAM_NAME: -- case PARAM_VALUE: -- parsedParam(buffer,value_length,param_name,param_value); -- break; -- } -- _values.add(buffer.toString()); -- } -- buffer.setLength(0); -- last_length=0; -- nws_length=0; -- value_length=param_name=param_value=-1; -- state=State.VALUE; -- continue; -- -- case '=': -- switch (state) -- { -- case VALUE: -- // It wasn't really a value, it was a param name -- value_length=param_name=0; -- buffer.setLength(nws_length); // trim following OWS -- String param = buffer.toString(); -- buffer.setLength(0); -- parsedValue(buffer); -- value_length=buffer.length(); -- buffer.append(param); -- buffer.append(c); -- last_length=++nws_length; -- state=State.PARAM_VALUE; -- continue; -- -- case PARAM_NAME: -- buffer.setLength(nws_length); // trim following OWS -- buffer.append(c); -- last_length=++nws_length; -- state=State.PARAM_VALUE; -- continue; -- -- case PARAM_VALUE: -- if (param_value<0) -- param_value=nws_length; -- buffer.append(c); -- nws_length=buffer.length(); -- continue; -- } -- continue; -- -- default: -- { -- switch (state) -- { -- case VALUE: -- { -- buffer.append(c); -- nws_length=buffer.length(); -- continue; -- } -- -- case PARAM_NAME: -- { -- if (param_name<0) -- param_name=nws_length; -- buffer.append(c); -- nws_length=buffer.length(); -- continue; -- } -- -- case PARAM_VALUE: -- { -- if (param_value<0) -- param_value=nws_length; -- buffer.append(c); -- nws_length=buffer.length(); -- continue; -- } -- } -- } -- } -- } -- } -- -- /** -- * Called when a value has been parsed -- * @param buffer Containing the trimmed value, which may be mutated -- */ -- protected void parsedValue(StringBuffer buffer) -- { -- } - -- /** -- * Called when a parameter has been parsed -- * @param buffer Containing the trimmed value and all parameters, which may be mutated -- * @param valueLength The length of the value -- * @param paramName The index of the start of the parameter just parsed -- * @param paramValue The index of the start of the parameter value just parsed, or -1 -- */ -- protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue) -+ @Override -+ protected void parsedValueAndParams(StringBuffer buffer) - { -+ _values.add(buffer.toString()); - } - - public int size() -@@ -274,55 +72,7 @@ public boolean isEmpty() - { - return _values.iterator(); - } -- -- public static String unquote(String s) -- { -- // handle trivial cases -- int l=s.length(); -- if (s==null || l==0) -- return s; -- -- // Look for any quotes -- int i=0; -- for (;ilast_length) // not leading OWS -+ buffer.append(c); -+ continue; -+ -+ case '"': -+ quoted=true; -+ if (_keepQuotes) -+ { -+ if (state==State.PARAM_VALUE && param_value<0) -+ param_value=nws_length; -+ buffer.append(c); -+ } -+ else if (state==State.PARAM_VALUE && param_value<0) -+ param_value=nws_length; -+ nws_length=buffer.length(); -+ continue; -+ -+ case ';': -+ buffer.setLength(nws_length); // trim following OWS -+ if (state==State.VALUE) -+ { -+ parsedValue(buffer); -+ value_length=buffer.length(); -+ } -+ else -+ parsedParam(buffer,value_length,param_name,param_value); -+ nws_length=buffer.length(); -+ param_name=param_value=-1; -+ buffer.append(c); -+ last_length=++nws_length; -+ state=State.PARAM_NAME; -+ continue; -+ -+ case ',': -+ case 0: -+ if (nws_length>0) -+ { -+ buffer.setLength(nws_length); // trim following OWS -+ switch(state) -+ { -+ case VALUE: -+ parsedValue(buffer); -+ value_length=buffer.length(); -+ break; -+ case PARAM_NAME: -+ case PARAM_VALUE: -+ parsedParam(buffer,value_length,param_name,param_value); -+ break; -+ } -+ parsedValueAndParams(buffer); -+ } -+ buffer.setLength(0); -+ last_length=0; -+ nws_length=0; -+ value_length=param_name=param_value=-1; -+ state=State.VALUE; -+ continue; -+ -+ case '=': -+ switch (state) -+ { -+ case VALUE: -+ // It wasn't really a value, it was a param name -+ value_length=param_name=0; -+ buffer.setLength(nws_length); // trim following OWS -+ String param = buffer.toString(); -+ buffer.setLength(0); -+ parsedValue(buffer); -+ value_length=buffer.length(); -+ buffer.append(param); -+ buffer.append(c); -+ last_length=++nws_length; -+ state=State.PARAM_VALUE; -+ continue; -+ -+ case PARAM_NAME: -+ buffer.setLength(nws_length); // trim following OWS -+ buffer.append(c); -+ last_length=++nws_length; -+ state=State.PARAM_VALUE; -+ continue; -+ -+ case PARAM_VALUE: -+ if (param_value<0) -+ param_value=nws_length; -+ buffer.append(c); -+ nws_length=buffer.length(); -+ continue; -+ } -+ continue; -+ -+ default: -+ { -+ switch (state) -+ { -+ case VALUE: -+ { -+ buffer.append(c); -+ nws_length=buffer.length(); -+ continue; -+ } -+ -+ case PARAM_NAME: -+ { -+ if (param_name<0) -+ param_name=nws_length; -+ buffer.append(c); -+ nws_length=buffer.length(); -+ continue; -+ } -+ -+ case PARAM_VALUE: -+ { -+ if (param_value<0) -+ param_value=nws_length; -+ buffer.append(c); -+ nws_length=buffer.length(); -+ continue; -+ } -+ } -+ } -+ } -+ } -+ } -+ -+ /** -+ * Called when a value and it's parameters has been parsed -+ * @param buffer Containing the trimmed value and parameters -+ */ -+ protected void parsedValueAndParams(StringBuffer buffer) -+ { -+ } -+ -+ /** -+ * Called when a value has been parsed (prior to any parameters) -+ * @param buffer Containing the trimmed value, which may be mutated -+ */ -+ protected void parsedValue(StringBuffer buffer) -+ { -+ } -+ -+ /** -+ * Called when a parameter has been parsed -+ * @param buffer Containing the trimmed value and all parameters, which may be mutated -+ * @param valueLength The length of the value -+ * @param paramName The index of the start of the parameter just parsed -+ * @param paramValue The index of the start of the parameter value just parsed, or -1 -+ */ -+ protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue) -+ { -+ } -+ -+} -diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ForwardedRequestCustomizer.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ForwardedRequestCustomizer.java -index be3990794ed..e098a68e635 100644 ---- a/jetty-server/src/main/java/org/eclipse/jetty/server/ForwardedRequestCustomizer.java -+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ForwardedRequestCustomizer.java -@@ -30,7 +30,7 @@ - import org.eclipse.jetty.http.HttpFields; - import org.eclipse.jetty.http.HttpHeader; - import org.eclipse.jetty.http.HttpScheme; --import org.eclipse.jetty.http.QuotedCSV; -+import org.eclipse.jetty.http.QuotedCSVParser; - import org.eclipse.jetty.server.HttpConfiguration.Customizer; - import org.eclipse.jetty.util.ArrayTrie; - import org.eclipse.jetty.util.HostPort; -@@ -42,8 +42,6 @@ - import static java.lang.invoke.MethodType.methodType; - - --/* ------------------------------------------------------------ */ -- - /** - * Customize Requests for Proxy Forwarding. - *

-@@ -157,7 +155,7 @@ public String getForcedHost() - */ - public void setForcedHost(String hostAndPort) - { -- _forcedHost = new HostPortHttpField(hostAndPort); -+ _forcedHost = new HostPortHttpField(new ForcedHostPort(hostAndPort)); - } - - /** -@@ -377,76 +375,26 @@ public void customize(Connector connector, HttpConfiguration config, Request req - throw new RuntimeException(e); - } - -- // Determine host -- if (_forcedHost != null) -- { -- // Update host header -- httpFields.put(_forcedHost); -- request.setAuthority(_forcedHost.getHost(), _forcedHost.getPort()); -- } -- else if (forwarded._rfc7239 != null && forwarded._rfc7239._host != null) -- { -- HostPortHttpField auth = forwarded._rfc7239._host; -- httpFields.put(auth); -- request.setAuthority(auth.getHost(), auth.getPort()); -- } -- else if (forwarded._forwardedHost != null) -- { -- HostPortHttpField auth = new HostPortHttpField(forwarded._forwardedHost); -- httpFields.put(auth); -- request.setAuthority(auth.getHost(), auth.getPort()); -- } -- else if (_proxyAsAuthority) -+ if (forwarded._proto!=null) - { -- if (forwarded._rfc7239 != null && forwarded._rfc7239._by != null) -- { -- HostPortHttpField auth = forwarded._rfc7239._by; -- httpFields.put(auth); -- request.setAuthority(auth.getHost(), auth.getPort()); -- } -- else if (forwarded._forwardedServer != null) -- { -- request.setAuthority(forwarded._forwardedServer, request.getServerPort()); -- } -+ request.setScheme(forwarded._proto); -+ if (forwarded._proto.equalsIgnoreCase(config.getSecureScheme())) -+ request.setSecure(true); - } - -- // handle remote end identifier -- if (forwarded._rfc7239 != null && forwarded._rfc7239._for != null) -- { -- request.setRemoteAddr(InetSocketAddress.createUnresolved(forwarded._rfc7239._for.getHost(), forwarded._rfc7239._for.getPort())); -- } -- else if (forwarded._forwardedFor != null) -+ if (forwarded._host!=null) - { -- int port = (forwarded._forwardedPort > 0) -- ? forwarded._forwardedPort -- : (forwarded._forwardedFor.getPort() > 0) -- ? forwarded._forwardedFor.getPort() -- : request.getRemotePort(); -- request.setRemoteAddr(InetSocketAddress.createUnresolved(forwarded._forwardedFor.getHost(), port)); -+ httpFields.put(new HostPortHttpField(forwarded._host)); -+ request.setAuthority(forwarded._host.getHost(), forwarded._host.getPort()); - } - -- // handle protocol identifier -- if (forwarded._rfc7239 != null && forwarded._rfc7239._proto != null) -+ if (forwarded._for!=null) - { -- request.setScheme(forwarded._rfc7239._proto); -- if (forwarded._rfc7239._proto.equals(config.getSecureScheme())) -- request.setSecure(true); -- } -- else if (forwarded._forwardedProto != null) -- { -- request.setScheme(forwarded._forwardedProto); -- if (forwarded._forwardedProto.equals(config.getSecureScheme())) -- request.setSecure(true); -- } -- else if (forwarded._forwardedHttps != null && ("on".equalsIgnoreCase(forwarded._forwardedHttps) || "true".equalsIgnoreCase(forwarded._forwardedHttps))) -- { -- request.setScheme(HttpScheme.HTTPS.asString()); -- if (HttpScheme.HTTPS.asString().equals(config.getSecureScheme())) -- request.setSecure(true); -+ int port = forwarded._for.getPort()>0 ? forwarded._for.getPort() : request.getRemotePort(); -+ request.setRemoteAddr(InetSocketAddress.createUnresolved(forwarded._for.getHost(),port)); - } - } - -- /* ------------------------------------------------------------ */ - protected String getLeftMost(String headerValue) - { - if (headerValue == null) -@@ -464,26 +412,6 @@ protected String getLeftMost(String headerValue) - return headerValue.substring(0, commaIndex).trim(); - } - -- protected HostPort getRemoteAddr(String headerValue) -- { -- String leftMost = getLeftMost(headerValue); -- if (leftMost == null) -- { -- return null; -- } -- -- try -- { -- return new HostPort(leftMost); -- } -- catch (Exception e) -- { -- // failed to parse in host[:port] format -- LOG.ignore(e); -- return null; -- } -- } -- - @Override - public String toString() - { -@@ -507,48 +435,6 @@ public void setHostHeader(String hostHeader) - _forcedHost = new HostPortHttpField(hostHeader); - } - -- private final class RFC7239 extends QuotedCSV -- { -- HostPortHttpField _by; -- HostPortHttpField _for; -- HostPortHttpField _host; -- String _proto; -- -- private RFC7239() -- { -- super(false); -- } -- -- @Override -- protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue) -- { -- if (valueLength == 0 && paramValue > paramName) -- { -- String name = StringUtil.asciiToLowerCase(buffer.substring(paramName, paramValue - 1)); -- String value = buffer.substring(paramValue); -- switch (name) -- { -- case "by": -- if (_by == null && !value.startsWith("_") && !"unknown".equals(value)) -- _by = new HostPortHttpField(value); -- break; -- case "for": -- if (_for == null && !value.startsWith("_") && !"unknown".equals(value)) -- _for = new HostPortHttpField(value); -- break; -- case "host": -- if (_host == null) -- _host = new HostPortHttpField(value); -- break; -- case "proto": -- if (_proto == null) -- _proto = value; -- break; -- } -- } -- } -- } -- - private void updateHandles() - { - int size = 0; -@@ -589,23 +475,52 @@ private void updateHandles() - } - } - -- private class Forwarded -+ private static class ForcedHostPort extends HostPort -+ { -+ ForcedHostPort(String authority) -+ { -+ super(authority); -+ } -+ } -+ -+ private static class XHostPort extends HostPort -+ { -+ XHostPort(String authority) -+ { -+ super(authority); -+ } -+ -+ XHostPort(String host, int port) -+ { -+ super(host, port); -+ } -+ } -+ -+ private static class Rfc7239HostPort extends HostPort -+ { -+ Rfc7239HostPort(String authority) -+ { -+ super(authority); -+ } -+ } -+ -+ private class Forwarded extends QuotedCSVParser - { - HttpConfiguration _config; - Request _request; - -- RFC7239 _rfc7239 = null; -- String _forwardedHost = null; -- String _forwardedServer = null; -- String _forwardedProto = null; -- HostPort _forwardedFor = null; -- int _forwardedPort = -1; -- String _forwardedHttps = null; -+ boolean _protoRfc7239; -+ String _proto; -+ HostPort _for; -+ HostPort _host; - - public Forwarded(Request request, HttpConfiguration config) - { -+ super(false); - _request = request; - _config = config; -+ if (_forcedHost!=null) -+ _host = _forcedHost.getHostPort(); - } - - public void handleCipherSuite(HttpField field) -@@ -630,39 +545,87 @@ public void handleSslSessionId(HttpField field) - - public void handleHost(HttpField field) - { -- _forwardedHost = getLeftMost(field.getValue()); -+ if (_host==null) -+ _host = new XHostPort(getLeftMost(field.getValue())); - } - - public void handleServer(HttpField field) - { -- _forwardedServer = getLeftMost(field.getValue()); -+ if (_proxyAsAuthority && _host==null) -+ _host = new XHostPort(getLeftMost(field.getValue())); - } - - public void handleProto(HttpField field) - { -- _forwardedProto = getLeftMost(field.getValue()); -+ if (_proto==null) -+ _proto = getLeftMost(field.getValue()); - } - - public void handleFor(HttpField field) - { -- _forwardedFor = getRemoteAddr(field.getValue()); -+ if (_for==null) -+ _for = new XHostPort(getLeftMost(field.getValue())); -+ else if (_for instanceof XHostPort && "unknown".equals(_for.getHost())) -+ _for = new XHostPort(HostPort.normalizeHost(getLeftMost(field.getValue())),_for.getPort()); - } - - public void handlePort(HttpField field) - { -- _forwardedPort = field.getIntValue(); -+ if (_for == null) -+ _for = new XHostPort("unknown", field.getIntValue()); -+ else if (_for instanceof XHostPort && _for.getPort()<=0) -+ _for = new XHostPort(HostPort.normalizeHost(_for.getHost()), field.getIntValue()); - } - - public void handleHttps(HttpField field) - { -- _forwardedHttps = getLeftMost(field.getValue()); -+ if (_proto==null && ("on".equalsIgnoreCase(field.getValue()) || "true".equalsIgnoreCase(field.getValue()))) -+ _proto = HttpScheme.HTTPS.asString(); - } - - public void handleRFC7239(HttpField field) - { -- if (_rfc7239 == null) -- _rfc7239 = new RFC7239(); -- _rfc7239.addValue(field.getValue()); -+ addValue(field.getValue()); -+ } -+ -+ @Override -+ protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue) -+ { -+ if (valueLength == 0 && paramValue > paramName) -+ { -+ String name = StringUtil.asciiToLowerCase(buffer.substring(paramName, paramValue - 1)); -+ String value = buffer.substring(paramValue); -+ switch (name) -+ { -+ case "by": -+ if (!_proxyAsAuthority) -+ break; -+ if (value.startsWith("_") || "unknown".equals(value)) -+ break; -+ if (_host == null || _host instanceof XHostPort) -+ _host = new Rfc7239HostPort(value); -+ break; -+ case "for": -+ if (value.startsWith("_") || "unknown".equals(value)) -+ break; -+ if (_for == null || _for instanceof XHostPort) -+ _for = new Rfc7239HostPort(value); -+ break; -+ case "host": -+ if (value.startsWith("_") || "unknown".equals(value)) -+ break; -+ if (_host == null || _host instanceof XHostPort) -+ _host = new Rfc7239HostPort(value); -+ break; -+ case "proto": -+ if (_proto == null || !_protoRfc7239) -+ { -+ _protoRfc7239 = true; -+ _proto = value; -+ } -+ break; -+ } -+ } - } - } - } -diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ForwardedRequestCustomizerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ForwardedRequestCustomizerTest.java -index 0a1cecec0c2..db8a9ad64e0 100644 ---- a/jetty-server/src/test/java/org/eclipse/jetty/server/ForwardedRequestCustomizerTest.java -+++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ForwardedRequestCustomizerTest.java -@@ -95,6 +95,68 @@ public void destroy() throws Exception - _server.join(); - } - -+ @Test -+ public void testHostIpv4() throws Exception -+ { -+ String response=_connector.getResponse( -+ "GET / HTTP/1.1\n"+ -+ "Host: 1.2.3.4:2222\n"+ -+ "\n"); -+ assertThat(response, Matchers.containsString("200 OK")); -+ assertEquals("http",_results.poll()); -+ assertEquals("1.2.3.4",_results.poll()); -+ assertEquals("2222",_results.poll()); -+ assertEquals("0.0.0.0",_results.poll()); -+ assertEquals("0",_results.poll()); -+ } -+ -+ @Test -+ public void testHostIpv6() throws Exception -+ { -+ String response=_connector.getResponse( -+ "GET / HTTP/1.1\n"+ -+ "Host: [::1]:2222\n"+ -+ "\n"); -+ assertThat(response, Matchers.containsString("200 OK")); -+ assertEquals("http",_results.poll()); -+ assertEquals("[::1]",_results.poll()); -+ assertEquals("2222",_results.poll()); -+ assertEquals("0.0.0.0",_results.poll()); -+ assertEquals("0",_results.poll()); -+ } -+ -+ -+ -+ @Test -+ public void testURIIpv4() throws Exception -+ { -+ String response=_connector.getResponse( -+ "GET http://1.2.3.4:2222/ HTTP/1.1\n"+ -+ "Host: wrong\n"+ -+ "\n"); -+ assertThat(response, Matchers.containsString("200 OK")); -+ assertEquals("http",_results.poll()); -+ assertEquals("1.2.3.4",_results.poll()); -+ assertEquals("2222",_results.poll()); -+ assertEquals("0.0.0.0",_results.poll()); -+ assertEquals("0",_results.poll()); -+ } -+ -+ @Test -+ public void testURIIpv6() throws Exception -+ { -+ String response=_connector.getResponse( -+ "GET http://[::1]:2222/ HTTP/1.1\n"+ -+ "Host: wrong\n"+ -+ "\n"); -+ assertThat(response, Matchers.containsString("200 OK")); -+ assertEquals("http",_results.poll()); -+ assertEquals("[::1]",_results.poll()); -+ assertEquals("2222",_results.poll()); -+ assertEquals("0.0.0.0",_results.poll()); -+ assertEquals("0",_results.poll()); -+ } -+ - - @Test - public void testRFC7239_Examples_4() throws Exception -@@ -208,6 +270,7 @@ public void testFor() throws Exception - "GET / HTTP/1.1\n"+ - "Host: myhost\n"+ - "X-Forwarded-For: 10.9.8.7,6.5.4.3\n"+ -+ "X-Forwarded-For: 8.9.8.7,7.5.4.3\n"+ - "\n"); - assertThat(response, Matchers.containsString("200 OK")); - assertEquals("http",_results.poll()); -@@ -264,6 +327,21 @@ public void testForIpv6AndPort() throws Exception - assertEquals("80",_results.poll()); - assertEquals("[1:2:3:4:5:6:7:8]",_results.poll()); - assertEquals("2222",_results.poll()); -+ -+ response=_connector.getResponse( -+ "GET / HTTP/1.1\n"+ -+ "Host: myhost\n"+ -+ "X-Forwarded-Port: 2222\n"+ -+ "X-Forwarded-For: 1:2:3:4:5:6:7:8\n"+ -+ "X-Forwarded-For: 7:7:7:7:7:7:7:7\n"+ -+ "X-Forwarded-Port: 3333\n"+ -+ "\n"); -+ assertThat(response, Matchers.containsString("200 OK")); -+ assertEquals("http",_results.poll()); -+ assertEquals("myhost",_results.poll()); -+ assertEquals("80",_results.poll()); -+ assertEquals("[1:2:3:4:5:6:7:8]",_results.poll()); -+ assertEquals("2222",_results.poll()); - } - - @Test -@@ -355,7 +433,26 @@ public void testSslCertificate() throws Exception - assertTrue(_wasSecure.get()); - assertEquals("0123456789abcdef",_sslCertificate.get()); - } -- -+ -+ -+ @Test -+ public void testMixed() throws Exception -+ { -+ String response = _connector.getResponse( -+ "GET / HTTP/1.1\n" + -+ "Host: myhost\n" + -+ "X-Forwarded-For: 11.9.8.7:1111,8.5.4.3:2222\n" + -+ "X-Forwarded-Port: 3333\n" + -+ "Forwarded: for=192.0.2.43,for=198.51.100.17;by=203.0.113.60;proto=http;host=example.com\n"+ -+ "X-Forwarded-For: 11.9.8.7:1111,8.5.4.3:2222\n" + -+ "\n"); -+ -+ assertEquals("http",_results.poll()); -+ assertEquals("example.com",_results.poll()); -+ assertEquals("80",_results.poll()); -+ assertEquals("192.0.2.43",_results.poll()); -+ assertEquals("0",_results.poll()); -+ } - - - interface RequestTester diff --git a/CVE-2020-27223.patch b/CVE-2020-27223.patch old mode 100644 new mode 100755 index ab24788..7d31ce4 --- a/CVE-2020-27223.patch +++ b/CVE-2020-27223.patch @@ -1,22 +1,670 @@ -From 10e531756b972162eed402c44d0244f7f6b85131 Mon Sep 17 00:00:00 2001 -From: Joakim Erdfelt -Date: Thu, 18 Feb 2021 07:14:38 -0600 -Subject: [PATCH] Merge pull request from GHSA-m394-8rww-3jr7 +From: Markus Koschany +Date: Sat, 31 Jul 2021 17:21:57 +0200 +Subject: CVE-2020-27223 -Use comparator based sort -Signed-off-by: Joakim Erdfelt -Signed-off-by: gregw - -Co-authored-by: gregw --- - .../eclipse/jetty/http/QuotedQualityCSV.java | 117 +++++++++++++----- - 1 file changed, 86 insertions(+), 31 deletions(-) + .../java/org/eclipse/jetty/http/QuotedCSV.java | 280 ++----------------- + .../org/eclipse/jetty/http/QuotedCSVParser.java | 303 +++++++++++++++++++++ + .../org/eclipse/jetty/http/QuotedQualityCSV.java | 140 ++++++---- + .../eclipse/jetty/http/QuotedQualityCSVTest.java | 143 +++++----- + 4 files changed, 479 insertions(+), 387 deletions(-) + create mode 100644 jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSVParser.java +diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSV.java b/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSV.java +index 9ca7dbe..a356213 100644 +--- a/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSV.java ++++ b/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSV.java +@@ -1,6 +1,6 @@ + // + // ======================================================================== +-// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. ++// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others. + // ------------------------------------------------------------------------ + // All rights reserved. This program and the accompanying materials + // are made available under the terms of the Eclipse Public License v1.0 +@@ -22,236 +22,36 @@ import java.util.ArrayList; + import java.util.Iterator; + import java.util.List; + +-import org.eclipse.jetty.util.QuotedStringTokenizer; +- +-/* ------------------------------------------------------------ */ + /** + * Implements a quoted comma separated list of values + * in accordance with RFC7230. + * OWS is removed and quoted characters ignored for parsing. ++ * + * @see "https://tools.ietf.org/html/rfc7230#section-3.2.6" + * @see "https://tools.ietf.org/html/rfc7230#section-7" + */ +-public class QuotedCSV implements Iterable +-{ +- private enum State { VALUE, PARAM_NAME, PARAM_VALUE}; +- ++public class QuotedCSV extends QuotedCSVParser implements Iterable ++{ + protected final List _values = new ArrayList<>(); +- protected final boolean _keepQuotes; +- +- /* ------------------------------------------------------------ */ ++ + public QuotedCSV(String... values) + { +- this(true,values); ++ this(true, values); + } +- +- /* ------------------------------------------------------------ */ +- public QuotedCSV(boolean keepQuotes,String... values) +- { +- _keepQuotes=keepQuotes; +- for (String v:values) +- addValue(v); +- } +- +- /* ------------------------------------------------------------ */ +- /** Add and parse a value string(s) +- * @param value A value that may contain one or more Quoted CSV items. +- */ +- public void addValue(String value) ++ ++ public QuotedCSV(boolean keepQuotes, String... values) + { +- if (value == null) +- return; +- +- StringBuffer buffer = new StringBuffer(); +- +- int l=value.length(); +- State state=State.VALUE; +- boolean quoted=false; +- boolean sloshed=false; +- int nws_length=0; +- int last_length=0; +- int value_length=-1; +- int param_name=-1; +- int param_value=-1; +- +- for (int i=0;i<=l;i++) ++ super(keepQuotes); ++ for (String v : values) + { +- char c=i==l?0:value.charAt(i); +- +- // Handle quoting https://tools.ietf.org/html/rfc7230#section-3.2.6 +- if (quoted && c!=0) +- { +- if (sloshed) +- sloshed=false; +- else +- { +- switch(c) +- { +- case '\\': +- sloshed=true; +- if (!_keepQuotes) +- continue; +- break; +- case '"': +- quoted=false; +- if (!_keepQuotes) +- continue; +- break; +- } +- } +- +- buffer.append(c); +- nws_length=buffer.length(); +- continue; +- } +- +- // Handle common cases +- switch(c) +- { +- case ' ': +- case '\t': +- if (buffer.length()>last_length) // not leading OWS +- buffer.append(c); +- continue; +- +- case '"': +- quoted=true; +- if (_keepQuotes) +- { +- if (state==State.PARAM_VALUE && param_value<0) +- param_value=nws_length; +- buffer.append(c); +- } +- else if (state==State.PARAM_VALUE && param_value<0) +- param_value=nws_length; +- nws_length=buffer.length(); +- continue; +- +- case ';': +- buffer.setLength(nws_length); // trim following OWS +- if (state==State.VALUE) +- { +- parsedValue(buffer); +- value_length=buffer.length(); +- } +- else +- parsedParam(buffer,value_length,param_name,param_value); +- nws_length=buffer.length(); +- param_name=param_value=-1; +- buffer.append(c); +- last_length=++nws_length; +- state=State.PARAM_NAME; +- continue; +- +- case ',': +- case 0: +- if (nws_length>0) +- { +- buffer.setLength(nws_length); // trim following OWS +- switch(state) +- { +- case VALUE: +- parsedValue(buffer); +- value_length=buffer.length(); +- break; +- case PARAM_NAME: +- case PARAM_VALUE: +- parsedParam(buffer,value_length,param_name,param_value); +- break; +- } +- _values.add(buffer.toString()); +- } +- buffer.setLength(0); +- last_length=0; +- nws_length=0; +- value_length=param_name=param_value=-1; +- state=State.VALUE; +- continue; +- +- case '=': +- switch (state) +- { +- case VALUE: +- // It wasn't really a value, it was a param name +- value_length=param_name=0; +- buffer.setLength(nws_length); // trim following OWS +- String param = buffer.toString(); +- buffer.setLength(0); +- parsedValue(buffer); +- value_length=buffer.length(); +- buffer.append(param); +- buffer.append(c); +- last_length=++nws_length; +- state=State.PARAM_VALUE; +- continue; +- +- case PARAM_NAME: +- buffer.setLength(nws_length); // trim following OWS +- buffer.append(c); +- last_length=++nws_length; +- state=State.PARAM_VALUE; +- continue; +- +- case PARAM_VALUE: +- if (param_value<0) +- param_value=nws_length; +- buffer.append(c); +- nws_length=buffer.length(); +- continue; +- } +- continue; +- +- default: +- { +- switch (state) +- { +- case VALUE: +- { +- buffer.append(c); +- nws_length=buffer.length(); +- continue; +- } +- +- case PARAM_NAME: +- { +- if (param_name<0) +- param_name=nws_length; +- buffer.append(c); +- nws_length=buffer.length(); +- continue; +- } +- +- case PARAM_VALUE: +- { +- if (param_value<0) +- param_value=nws_length; +- buffer.append(c); +- nws_length=buffer.length(); +- continue; +- } +- } +- } +- } ++ addValue(v); + } + } + +- /** +- * Called when a value has been parsed +- * @param buffer Containing the trimmed value, which may be mutated +- */ +- protected void parsedValue(StringBuffer buffer) +- { +- } +- +- /** +- * Called when a parameter has been parsed +- * @param buffer Containing the trimmed value and all parameters, which may be mutated +- * @param valueLength The length of the value +- * @param paramName The index of the start of the parameter just parsed +- * @param paramValue The index of the start of the parameter value just parsed, or -1 +- */ +- protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue) ++ @Override ++ protected void parsedValueAndParams(StringBuffer buffer) + { ++ _values.add(buffer.toString()); + } + + public int size() +@@ -268,67 +68,21 @@ public class QuotedCSV implements Iterable + { + return _values; + } +- ++ + @Override + public Iterator iterator() + { + return _values.iterator(); + } +- +- public static String unquote(String s) +- { +- // handle trivial cases +- int l=s.length(); +- if (s==null || l==0) +- return s; +- +- // Look for any quotes +- int i=0; +- for (;i list = new ArrayList<>(); + for (String s : this) ++ { + list.add(s); ++ } + return list.toString(); + } + } +diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSVParser.java b/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSVParser.java +new file mode 100644 +index 0000000..7aefcf7 +--- /dev/null ++++ b/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSVParser.java +@@ -0,0 +1,303 @@ ++// ++// ======================================================================== ++// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others. ++// ------------------------------------------------------------------------ ++// 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.http; ++ ++/** ++ * Implements a quoted comma separated list parser ++ * in accordance with RFC7230. ++ * OWS is removed and quoted characters ignored for parsing. ++ * ++ * @see "https://tools.ietf.org/html/rfc7230#section-3.2.6" ++ * @see "https://tools.ietf.org/html/rfc7230#section-7" ++ */ ++public abstract class QuotedCSVParser ++{ ++ private enum State ++ { ++ VALUE, PARAM_NAME, PARAM_VALUE ++ } ++ ++ protected final boolean _keepQuotes; ++ ++ public QuotedCSVParser(boolean keepQuotes) ++ { ++ _keepQuotes = keepQuotes; ++ } ++ ++ public static String unquote(String s) ++ { ++ // handle trivial cases ++ int l = s.length(); ++ if (s == null || l == 0) ++ return s; ++ ++ // Look for any quotes ++ int i = 0; ++ for (; i < l; i++) ++ { ++ char c = s.charAt(i); ++ if (c == '"') ++ break; ++ } ++ if (i == l) ++ return s; ++ ++ boolean quoted = true; ++ boolean sloshed = false; ++ StringBuffer buffer = new StringBuffer(); ++ buffer.append(s, 0, i); ++ i++; ++ for (; i < l; i++) ++ { ++ char c = s.charAt(i); ++ if (quoted) ++ { ++ if (sloshed) ++ { ++ buffer.append(c); ++ sloshed = false; ++ } ++ else if (c == '"') ++ quoted = false; ++ else if (c == '\\') ++ sloshed = true; ++ else ++ buffer.append(c); ++ } ++ else if (c == '"') ++ quoted = true; ++ else ++ buffer.append(c); ++ } ++ return buffer.toString(); ++ } ++ ++ /** ++ * Add and parse a value string(s) ++ * ++ * @param value A value that may contain one or more Quoted CSV items. ++ */ ++ public void addValue(String value) ++ { ++ if (value == null) ++ return; ++ ++ StringBuffer buffer = new StringBuffer(); ++ ++ int l = value.length(); ++ State state = State.VALUE; ++ boolean quoted = false; ++ boolean sloshed = false; ++ int nwsLength = 0; ++ int lastLength = 0; ++ int valueLength = -1; ++ int paramName = -1; ++ int paramValue = -1; ++ ++ for (int i = 0; i <= l; i++) ++ { ++ char c = i == l ? 0 : value.charAt(i); ++ ++ // Handle quoting https://tools.ietf.org/html/rfc7230#section-3.2.6 ++ if (quoted && c != 0) ++ { ++ if (sloshed) ++ sloshed = false; ++ else ++ { ++ switch (c) ++ { ++ case '\\': ++ sloshed = true; ++ if (!_keepQuotes) ++ continue; ++ break; ++ case '"': ++ quoted = false; ++ if (!_keepQuotes) ++ continue; ++ break; ++ } ++ } ++ ++ buffer.append(c); ++ nwsLength = buffer.length(); ++ continue; ++ } ++ ++ // Handle common cases ++ switch (c) ++ { ++ case ' ': ++ case '\t': ++ if (buffer.length() > lastLength) // not leading OWS ++ buffer.append(c); ++ continue; ++ ++ case '"': ++ quoted = true; ++ if (_keepQuotes) ++ { ++ if (state == State.PARAM_VALUE && paramValue < 0) ++ paramValue = nwsLength; ++ buffer.append(c); ++ } ++ else if (state == State.PARAM_VALUE && paramValue < 0) ++ paramValue = nwsLength; ++ nwsLength = buffer.length(); ++ continue; ++ ++ case ';': ++ buffer.setLength(nwsLength); // trim following OWS ++ if (state == State.VALUE) ++ { ++ parsedValue(buffer); ++ valueLength = buffer.length(); ++ } ++ else ++ parsedParam(buffer, valueLength, paramName, paramValue); ++ nwsLength = buffer.length(); ++ paramName = paramValue = -1; ++ buffer.append(c); ++ lastLength = ++nwsLength; ++ state = State.PARAM_NAME; ++ continue; ++ ++ case ',': ++ case 0: ++ if (nwsLength > 0) ++ { ++ buffer.setLength(nwsLength); // trim following OWS ++ switch (state) ++ { ++ case VALUE: ++ parsedValue(buffer); ++ valueLength = buffer.length(); ++ break; ++ case PARAM_NAME: ++ case PARAM_VALUE: ++ parsedParam(buffer, valueLength, paramName, paramValue); ++ break; ++ } ++ parsedValueAndParams(buffer); ++ } ++ buffer.setLength(0); ++ lastLength = 0; ++ nwsLength = 0; ++ valueLength = paramName = paramValue = -1; ++ state = State.VALUE; ++ continue; ++ ++ case '=': ++ switch (state) ++ { ++ case VALUE: ++ // It wasn't really a value, it was a param name ++ valueLength = paramName = 0; ++ buffer.setLength(nwsLength); // trim following OWS ++ String param = buffer.toString(); ++ buffer.setLength(0); ++ parsedValue(buffer); ++ valueLength = buffer.length(); ++ buffer.append(param); ++ buffer.append(c); ++ lastLength = ++nwsLength; ++ state = State.PARAM_VALUE; ++ continue; ++ ++ case PARAM_NAME: ++ buffer.setLength(nwsLength); // trim following OWS ++ buffer.append(c); ++ lastLength = ++nwsLength; ++ state = State.PARAM_VALUE; ++ continue; ++ ++ case PARAM_VALUE: ++ if (paramValue < 0) ++ paramValue = nwsLength; ++ buffer.append(c); ++ nwsLength = buffer.length(); ++ continue; ++ } ++ continue; ++ ++ default: ++ { ++ switch (state) ++ { ++ case VALUE: ++ { ++ buffer.append(c); ++ nwsLength = buffer.length(); ++ continue; ++ } ++ ++ case PARAM_NAME: ++ { ++ if (paramName < 0) ++ paramName = nwsLength; ++ buffer.append(c); ++ nwsLength = buffer.length(); ++ continue; ++ } ++ ++ case PARAM_VALUE: ++ { ++ if (paramValue < 0) ++ paramValue = nwsLength; ++ buffer.append(c); ++ nwsLength = buffer.length(); ++ continue; ++ } ++ } ++ } ++ } ++ } ++ } ++ ++ /** ++ * Called when a value and it's parameters has been parsed ++ * ++ * @param buffer Containing the trimmed value and parameters ++ */ ++ protected void parsedValueAndParams(StringBuffer buffer) ++ { ++ } ++ ++ /** ++ * Called when a value has been parsed (prior to any parameters) ++ * ++ * @param buffer Containing the trimmed value, which may be mutated ++ */ ++ protected void parsedValue(StringBuffer buffer) ++ { ++ } ++ ++ /** ++ * Called when a parameter has been parsed ++ * ++ * @param buffer Containing the trimmed value and all parameters, which may be mutated ++ * @param valueLength The length of the value ++ * @param paramName The index of the start of the parameter just parsed ++ * @param paramValue The index of the start of the parameter value just parsed, or -1 ++ */ ++ protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue) ++ { ++ } ++} diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java b/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java -index d148d9e..67f9981 100644 +index d148d9e..5bc9985 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java -@@ -21,12 +21,12 @@ package org.eclipse.jetty.http; +@@ -1,6 +1,6 @@ + // + // ======================================================================== +-// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. ++// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others. + // ------------------------------------------------------------------------ + // All rights reserved. This program and the accompanying materials + // are made available under the terms of the Eclipse Public License v1.0 +@@ -21,14 +21,12 @@ package org.eclipse.jetty.http; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -28,10 +676,12 @@ index d148d9e..67f9981 100644 -import static java.lang.Integer.MIN_VALUE; - - /* ------------------------------------------------------------ */ - +-/* ------------------------------------------------------------ */ +- /** -@@ -57,7 +57,8 @@ public class QuotedQualityCSV extends QuotedCSV implements Iterable + * Implements a quoted comma separated list of quality values + * in accordance with RFC7230 and RFC7231. +@@ -57,22 +55,19 @@ public class QuotedQualityCSV extends QuotedCSV implements Iterable return 3; }; @@ -41,7 +691,10 @@ index d148d9e..67f9981 100644 private boolean _sorted = false; private final ToIntFunction _secondaryOrdering; -@@ -68,7 +69,7 @@ public class QuotedQualityCSV extends QuotedCSV implements Iterable +- /* ------------------------------------------------------------ */ +- + /** + * Sorts values with equal quality according to the length of the value String. */ public QuotedQualityCSV() { @@ -49,8 +702,20 @@ index d148d9e..67f9981 100644 + this((ToIntFunction)null); } - /* ------------------------------------------------------------ */ -@@ -89,7 +90,7 @@ public class QuotedQualityCSV extends QuotedCSV implements Iterable +- /* ------------------------------------------------------------ */ +- + /** + * Sorts values with equal quality according to given order. + * +@@ -83,57 +78,71 @@ public class QuotedQualityCSV extends QuotedCSV implements Iterable + this((s) -> + { + for (int i = 0; i < preferredOrder.length; ++i) ++ { + if (preferredOrder[i].equals(s)) + return preferredOrder.length - i; ++ } + if ("*".equals(s)) return preferredOrder.length; @@ -59,7 +724,8 @@ index d148d9e..67f9981 100644 }); } -@@ -98,27 +99,43 @@ public class QuotedQualityCSV extends QuotedCSV implements Iterable +- /* ------------------------------------------------------------ */ +- /** * Orders values with equal quality with the given function. * @@ -71,50 +737,61 @@ index d148d9e..67f9981 100644 this._secondaryOrdering = secondaryOrdering == null ? s -> 0 : secondaryOrdering; } +- /* ------------------------------------------------------------ */ + @Override + protected void parsedValueAndParams(StringBuffer buffer) + { + super.parsedValueAndParams(buffer); + -+ // Collect full value with parameters -+ _lastQuality = new QualityValue(_lastQuality._quality, buffer.toString(), _lastQuality._index); -+ _qualities.set(_lastQuality._index, _lastQuality); ++ // Collect full value with parameters ++ _lastQuality = new QualityValue(_lastQuality._quality, buffer.toString(), _lastQuality._index); ++ _qualities.set(_lastQuality._index, _lastQuality); + } + - /* ------------------------------------------------------------ */ @Override protected void parsedValue(StringBuffer buffer) { super.parsedValue(buffer); -+ _sorted = false; ++ _sorted = false; + + // This is the just the value, without parameters. // Assume a quality of ONE - _quality.add(1.0D); -+ _lastQuality = new QualityValue(1.0D, buffer.toString(), _qualities.size()); -+ _qualities.add(_lastQuality); ++ _lastQuality = new QualityValue(1.0D, buffer.toString(), _qualities.size()); ++ _qualities.add(_lastQuality); } - /* ------------------------------------------------------------ */ +- /* ------------------------------------------------------------ */ @Override protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue) { -+ _sorted = false; ++ _sorted = false; + if (paramName < 0) { if (buffer.charAt(buffer.length() - 1) == ';') -@@ -128,7 +145,7 @@ public class QuotedQualityCSV extends QuotedCSV implements Iterable - buffer.charAt(paramName) == 'q' && paramValue > paramName && - buffer.length() >= paramName && buffer.charAt(paramName + 1) == '=') + buffer.setLength(buffer.length() - 1); + } + else if (paramValue >= 0 && +- buffer.charAt(paramName) == 'q' && paramValue > paramName && +- buffer.length() >= paramName && buffer.charAt(paramName + 1) == '=') ++ buffer.charAt(paramName) == 'q' && paramValue > paramName && ++ buffer.length() >= paramName && buffer.charAt(paramName + 1) == '=') { - Double q; + double q; try { q = (_keepQuotes && buffer.charAt(paramValue) == '"') -@@ -143,8 +160,10 @@ public class QuotedQualityCSV extends QuotedCSV implements Iterable +- ? Double.valueOf(buffer.substring(paramValue + 1, buffer.length() - 1)) +- : Double.valueOf(buffer.substring(paramValue)); ++ ? Double.valueOf(buffer.substring(paramValue + 1, buffer.length() - 1)) ++ : Double.valueOf(buffer.substring(paramValue)); + } + catch (Exception e) + { +@@ -143,8 +152,10 @@ public class QuotedQualityCSV extends QuotedCSV implements Iterable buffer.setLength(Math.max(0, paramName - 1)); if (q != 1.0D) @@ -127,65 +804,65 @@ index d148d9e..67f9981 100644 } } -@@ -166,38 +185,74 @@ public class QuotedQualityCSV extends QuotedCSV implements Iterable +@@ -166,38 +177,73 @@ public class QuotedQualityCSV extends QuotedCSV implements Iterable protected void sort() { -+ _values.clear(); -+ _qualities.stream() -+ .filter((qv) -> qv._quality != 0.0D) -+ .sorted() -+ .map(QualityValue::getValue) -+ .collect(Collectors.toCollection(() -> _values)); ++ _values.clear(); ++ _qualities.stream() ++ .filter((qv) -> qv._quality != 0.0D) ++ .sorted() ++ .map(QualityValue::getValue) ++ .collect(Collectors.toCollection(() -> _values)); _sorted = true; + } - -- Double last = 0.0D; -- int lastSecondaryOrder = Integer.MIN_VALUE; ++ + private class QualityValue implements Comparable + { + private final double _quality; -+ private final String _value; -+ private final int _index; ++ private final String _value; ++ private final int _index; + +- Double last = 0.0D; +- int lastSecondaryOrder = Integer.MIN_VALUE; ++ private QualityValue(double quality, String value, int index) ++ { ++ _quality = quality; ++ _value = value; ++ _index = index; ++ } - for (int i = _values.size(); i-- > 0; ) -+ private QualityValue(double quality, String value, int index) ++ @Override ++ public int hashCode() { - String v = _values.get(i); - Double q = _quality.get(i); -+ _quality = quality; -+ _value = value; -+ _index = index; -+ } -+ -+ @Override -+ public int hashCode() -+ { -+ return Double.hashCode(_quality) ^ Objects.hash(_value, _index); -+ } -+ -+ @Override -+ public boolean equals(Object obj) -+ { -+ if (!(obj instanceof QualityValue)) -+ return false; -+ QualityValue qv = (QualityValue)obj; -+ return _quality == qv._quality && Objects.equals(_value, qv._value) && Objects.equals(_index, qv._index); -+ } -+ -+ private String getValue() -+ { -+ return _value; -+ } ++ return Double.hashCode(_quality) ^ Objects.hash(_value, _index); ++ } - int compare = last.compareTo(q); - if (compare > 0 || (compare == 0 && _secondaryOrdering.applyAsInt(v) < lastSecondaryOrder)) + @Override -+ public int compareTo(QualityValue o) -+ { -+ // sort highest quality first -+ int compare = Double.compare(o._quality, _quality); -+ if (compare == 0) ++ public boolean equals(Object obj) ++ { ++ if (!(obj instanceof QualityValue)) ++ return false; ++ QualityValue qv = (QualityValue)obj; ++ return _quality == qv._quality && Objects.equals(_value, qv._value) && Objects.equals(_index, qv._index); ++ } ++ ++ private String getValue() ++ { ++ return _value; ++ } ++ ++ @Override ++ public int compareTo(QualityValue o) ++ { ++ // sort highest quality first ++ int compare = Double.compare(o._quality, _quality); ++ if (compare == 0) { - _values.set(i, _values.get(i + 1)); - _values.set(i + 1, v); @@ -195,34 +872,325 @@ index d148d9e..67f9981 100644 - lastSecondaryOrder = 0; - i = _values.size(); - continue; -+ // then sort secondary order highest first -+ compare = Integer.compare(_secondaryOrdering.applyAsInt(o._value), _secondaryOrdering.applyAsInt(_value)); -+ if (compare == 0) -+ // then sort index lowest first -+ compare = -Integer.compare(o._index, _index); ++ // then sort secondary order highest first ++ compare = Integer.compare(_secondaryOrdering.applyAsInt(o._value), _secondaryOrdering.applyAsInt(_value)); ++ if (compare == 0) ++ // then sort index lowest first ++ compare = -Integer.compare(o._index, _index); } - +- - last = q; - lastSecondaryOrder = _secondaryOrdering.applyAsInt(v); -+ return compare; ++ return compare; } - int last_element = _quality.size(); - while (last_element > 0 && _quality.get(--last_element).equals(0.0D)) -+ @Override -+ public String toString() ++ @Override ++ public String toString() { - _quality.remove(last_element); - _values.remove(last_element); + return String.format("%s@%x[%s,q=%f,i=%d]", + getClass().getSimpleName(), -+ hashCode(), -+ _value, -+ _quality, -+ _index); ++ hashCode(), ++ _value, ++ _quality, ++ _index); } } } --- -2.23.0 - +diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/QuotedQualityCSVTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/QuotedQualityCSVTest.java +index f03657b..b941e95 100644 +--- a/jetty-http/src/test/java/org/eclipse/jetty/http/QuotedQualityCSVTest.java ++++ b/jetty-http/src/test/java/org/eclipse/jetty/http/QuotedQualityCSVTest.java +@@ -1,6 +1,6 @@ + // + // ======================================================================== +-// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. ++// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others. + // ------------------------------------------------------------------------ + // All rights reserved. This program and the accompanying materials + // are made available under the terms of the Eclipse Public License v1.0 +@@ -18,13 +18,12 @@ + + package org.eclipse.jetty.http; + ++import java.util.ArrayList; ++import java.util.List; + + import org.hamcrest.Matchers; + import org.junit.jupiter.api.Test; + +-import java.util.ArrayList; +-import java.util.List; +- + import static org.hamcrest.MatcherAssert.assertThat; + import static org.hamcrest.Matchers.contains; + +@@ -32,58 +31,58 @@ public class QuotedQualityCSVTest + { + + @Test +- public void test7231_5_3_2_example1() ++ public void test7231Sec532Example1() + { + QuotedQualityCSV values = new QuotedQualityCSV(); + values.addValue(" audio/*; q=0.2, audio/basic"); +- assertThat(values,Matchers.contains("audio/basic","audio/*")); ++ assertThat(values, Matchers.contains("audio/basic", "audio/*")); + } + + @Test +- public void test7231_5_3_2_example2() ++ public void test7231Sec532Example2() + { + QuotedQualityCSV values = new QuotedQualityCSV(); + values.addValue("text/plain; q=0.5, text/html,"); + values.addValue("text/x-dvi; q=0.8, text/x-c"); +- assertThat(values,Matchers.contains("text/html","text/x-c","text/x-dvi","text/plain")); ++ assertThat(values, Matchers.contains("text/html", "text/x-c", "text/x-dvi", "text/plain")); + } +- ++ + @Test +- public void test7231_5_3_2_example3() ++ public void test7231Sec532Example3() + { + QuotedQualityCSV values = new QuotedQualityCSV(); + values.addValue("text/*, text/plain, text/plain;format=flowed, */*"); +- ++ + // Note this sort is only on quality and not the most specific type as per 5.3.2 +- assertThat(values,Matchers.contains("text/*","text/plain","text/plain;format=flowed","*/*")); ++ assertThat(values, Matchers.contains("text/*", "text/plain", "text/plain;format=flowed", "*/*")); + } +- ++ + @Test +- public void test7231_5_3_2_example3_most_specific() ++ public void test7231532Example3MostSpecific() + { + QuotedQualityCSV values = new QuotedQualityCSV(QuotedQualityCSV.MOST_SPECIFIC_MIME_ORDERING); + values.addValue("text/*, text/plain, text/plain;format=flowed, */*"); +- +- assertThat(values,Matchers.contains("text/plain;format=flowed","text/plain","text/*","*/*")); ++ ++ assertThat(values, Matchers.contains("text/plain;format=flowed", "text/plain", "text/*", "*/*")); + } +- ++ + @Test +- public void test7231_5_3_2_example4() ++ public void test7231Sec532Example4() + { + QuotedQualityCSV values = new QuotedQualityCSV(); + values.addValue("text/*;q=0.3, text/html;q=0.7, text/html;level=1,"); + values.addValue("text/html;level=2;q=0.4, */*;q=0.5"); +- assertThat(values,Matchers.contains( +- "text/html;level=1", +- "text/html", +- "*/*", +- "text/html;level=2", +- "text/*" +- )); ++ assertThat(values, Matchers.contains( ++ "text/html;level=1", ++ "text/html", ++ "*/*", ++ "text/html;level=2", ++ "text/*" ++ )); + } +- ++ + @Test +- public void test7231_5_3_4_example1() ++ public void test7231Sec534Example1() + { + QuotedQualityCSV values = new QuotedQualityCSV(); + values.addValue("compress, gzip"); +@@ -91,16 +90,16 @@ public class QuotedQualityCSVTest + values.addValue("*"); + values.addValue("compress;q=0.5, gzip;q=1.0"); + values.addValue("gzip;q=1.0, identity; q=0.5, *;q=0"); +- +- assertThat(values,Matchers.contains( +- "compress", +- "gzip", +- "*", +- "gzip", +- "gzip", +- "compress", +- "identity" +- )); ++ ++ assertThat(values, Matchers.contains( ++ "compress", ++ "gzip", ++ "*", ++ "gzip", ++ "gzip", ++ "compress", ++ "identity" ++ )); + } + + @Test +@@ -108,66 +107,65 @@ public class QuotedQualityCSVTest + { + QuotedQualityCSV values = new QuotedQualityCSV(); + values.addValue(" value 0.5 ; p = v ; q =0.5 , value 1.0 "); +- assertThat(values,Matchers.contains( +- "value 1.0", +- "value 0.5;p=v")); ++ assertThat(values, Matchers.contains( ++ "value 1.0", ++ "value 0.5;p=v")); + } +- ++ + @Test + public void testEmpty() + { + QuotedQualityCSV values = new QuotedQualityCSV(); + values.addValue(",aaaa, , bbbb ,,cccc,"); +- assertThat(values,Matchers.contains( +- "aaaa", +- "bbbb", +- "cccc")); ++ assertThat(values, Matchers.contains( ++ "aaaa", ++ "bbbb", ++ "cccc")); + } +- ++ + @Test + public void testQuoted() + { + QuotedQualityCSV values = new QuotedQualityCSV(); + values.addValue(" value 0.5 ; p = \"v ; q = \\\"0.5\\\" , value 1.0 \" "); +- assertThat(values,Matchers.contains( +- "value 0.5;p=\"v ; q = \\\"0.5\\\" , value 1.0 \"")); ++ assertThat(values, Matchers.contains( ++ "value 0.5;p=\"v ; q = \\\"0.5\\\" , value 1.0 \"")); + } +- ++ + @Test + public void testOpenQuote() + { + QuotedQualityCSV values = new QuotedQualityCSV(); + values.addValue("value;p=\"v"); +- assertThat(values,Matchers.contains( +- "value;p=\"v")); ++ assertThat(values, Matchers.contains( ++ "value;p=\"v")); + } +- ++ + @Test + public void testQuotedQuality() + { + QuotedQualityCSV values = new QuotedQualityCSV(); + values.addValue(" value 0.5 ; p = v ; q = \"0.5\" , value 1.0 "); +- assertThat(values,Matchers.contains( +- "value 1.0", +- "value 0.5;p=v")); ++ assertThat(values, Matchers.contains( ++ "value 1.0", ++ "value 0.5;p=v")); + } +- ++ + @Test + public void testBadQuality() + { + QuotedQualityCSV values = new QuotedQualityCSV(); + values.addValue("value0.5;p=v;q=0.5,value1.0,valueBad;q=X"); +- assertThat(values,Matchers.contains( +- "value1.0", +- "value0.5;p=v")); ++ assertThat(values, Matchers.contains( ++ "value1.0", ++ "value0.5;p=v")); + } +- ++ + @Test + public void testBad() + { + QuotedQualityCSV values = new QuotedQualityCSV(); + +- + // None of these should throw exceptions + values.addValue(null); + values.addValue(""); +@@ -223,13 +221,10 @@ public class QuotedQualityCSVTest + values.addValue("q="); + values.addValue("q=,"); + values.addValue("q=;"); +- + } + +- /* ------------------------------------------------------------ */ +- +- private static final String[] preferBrotli = {"br","gzip"}; +- private static final String[] preferGzip = {"gzip","br"}; ++ private static final String[] preferBrotli = {"br", "gzip"}; ++ private static final String[] preferGzip = {"gzip", "br"}; + private static final String[] noFormats = {}; + + @Test +@@ -295,14 +290,13 @@ public class QuotedQualityCSVTest + values.addValue("gzip, *"); + assertThat(values, contains("*", "gzip")); + } +- + + @Test + public void testSameQuality() + { + QuotedQualityCSV values = new QuotedQualityCSV(); + values.addValue("one;q=0.5,two;q=0.5,three;q=0.5"); +- assertThat(values.getValues(),Matchers.contains("one","two","three")); ++ assertThat(values.getValues(), Matchers.contains("one", "two", "three")); + } + + @Test +@@ -310,10 +304,9 @@ public class QuotedQualityCSVTest + { + QuotedQualityCSV values = new QuotedQualityCSV(); + values.addValue("one,two;,three;x=y"); +- assertThat(values.getValues(),Matchers.contains("one","two","three;x=y")); ++ assertThat(values.getValues(), Matchers.contains("one", "two", "three;x=y")); + } + +- + @Test + public void testQuality() + { +@@ -339,19 +332,15 @@ public class QuotedQualityCSVTest + } + }; + +- + // The provided string is not legal according to some RFCs ( not a token because of = and not a parameter because not preceded by ; ) + // The string is legal according to RFC7239 which allows for just parameters (called forwarded-pairs) + values.addValue("p=0.5,q=0.5"); + +- + // The QuotedCSV implementation is lenient and adopts the later interpretation and thus sees q=0.5 and p=0.5 both as parameters +- assertThat(results,contains("parsedValue: ", "parsedParam: p=0.5", +- "parsedValue: ", "parsedParam: q=0.5")); +- ++ assertThat(results, contains("parsedValue: ", "parsedParam: p=0.5", ++ "parsedValue: ", "parsedParam: q=0.5")); + + // However the QuotedQualityCSV only handles the q parameter and that is consumed from the parameter string. +- assertThat(values,contains("p=0.5", "")); +- ++ assertThat(values, contains("p=0.5", "")); + } + } diff --git a/CVE-2021-28165-1.patch b/CVE-2021-28165-1.patch deleted file mode 100644 index 585049c..0000000 --- a/CVE-2021-28165-1.patch +++ /dev/null @@ -1,36 +0,0 @@ -From 00d379c94ba865dced2025c2d1bc3e2e0e41e880 Mon Sep 17 00:00:00 2001 -From: Joakim Erdfelt -Date: Thu, 18 Mar 2021 08:08:55 -0500 -Subject: [PATCH] Fixes #6072 - jetty server high CPU when client send data - length > 17408. - -Avoid spinning if the input buffer is full. - -Signed-off-by: Simone Bordet -Co-authored-by: Joakim Erdfelt ---- - .../main/java/org/eclipse/jetty/io/ssl/SslConnection.java | 8 +++++++- - 1 file changed, 7 insertions(+), 1 deletion(-) - -diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java -index bc2431d..b2482e7 100644 ---- a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java -+++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java -@@ -603,7 +603,13 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr - - case BUFFER_UNDERFLOW: - if (net_filled > 0) -- continue; // try filling some more -+ { -+ if (BufferUtil.space(_encryptedInput) > 0) -+ continue; // try filling some more -+ BufferUtil.clear(_encryptedInput); -+ throw new SSLHandshakeException("Encrypted buffer max length exceeded"); -+ } -+ - _underflown = true; - if (net_filled < 0 && _sslEngine.getUseClientMode()) - { --- -2.23.0 - diff --git a/CVE-2021-28165-2.patch b/CVE-2021-28165-2.patch deleted file mode 100644 index 3063410..0000000 --- a/CVE-2021-28165-2.patch +++ /dev/null @@ -1,39 +0,0 @@ -From 294b2ba02b667548617a94cd99592110ac230add Mon Sep 17 00:00:00 2001 -From: Simone Bordet -Date: Mon, 22 Mar 2021 10:39:36 +0100 -Subject: [PATCH] Fixes #6072 - jetty server high CPU when client send data - length > 17408. - -Updates after review. - -Signed-off-by: Simone Bordet ---- - .../main/java/org/eclipse/jetty/io/ssl/SslConnection.java | 7 ++++--- - 1 file changed, 4 insertions(+), 3 deletions(-) - -diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java -index b2482e7..44c7f10 100644 ---- a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java -+++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java -@@ -602,14 +602,15 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr - return filled = -1; - - case BUFFER_UNDERFLOW: -- if (net_filled > 0) -+ if (BufferUtil.space(_encryptedInput) == 0) - { -- if (BufferUtil.space(_encryptedInput) > 0) -- continue; // try filling some more - BufferUtil.clear(_encryptedInput); - throw new SSLHandshakeException("Encrypted buffer max length exceeded"); - } - -+ if (net_filled > 0) -+ continue; // try filling some more -+ - _underflown = true; - if (net_filled < 0 && _sslEngine.getUseClientMode()) - { --- -2.23.0 - diff --git a/CVE-2021-28165.patch b/CVE-2021-28165.patch new file mode 100755 index 0000000..5d1f6a9 --- /dev/null +++ b/CVE-2021-28165.patch @@ -0,0 +1,533 @@ +From: Markus Koschany +Date: Sat, 31 Jul 2021 17:24:07 +0200 +Subject: CVE-2021-28165 + +--- + .../org/eclipse/jetty/io/ssl/SslConnection.java | 12 + + .../eclipse/jetty/server/ssl/SSLEngineTest.java | 267 +++++++++++++-------- + 2 files changed, 183 insertions(+), 96 deletions(-) + +diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java +index a2c1fdc..c385f27 100644 +--- a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java ++++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java +@@ -330,6 +330,11 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr + _decryptedEndPoint.onFillableFail(cause == null ? new IOException() : cause); + } + ++ protected SSLEngineResult unwrap(SSLEngine sslEngine, ByteBuffer input, ByteBuffer output) throws SSLException ++ { ++ return sslEngine.unwrap(input, output); ++ } ++ + @Override + public String toConnectionString() + { +@@ -602,8 +607,15 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr + return filled = -1; + + case BUFFER_UNDERFLOW: ++ if (BufferUtil.space(_encryptedInput) == 0) ++ { ++ BufferUtil.clear(_encryptedInput); ++ throw new SSLHandshakeException("Encrypted buffer max length exceeded"); ++ } ++ + if (net_filled > 0) + continue; // try filling some more ++ + _underflown = true; + if (net_filled < 0 && _sslEngine.getUseClientMode()) + { +diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLEngineTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLEngineTest.java +index ae6a5b6..aa1b9c9 100644 +--- a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLEngineTest.java ++++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLEngineTest.java +@@ -1,6 +1,6 @@ + // + // ======================================================================== +-// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. ++// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others. + // ------------------------------------------------------------------------ + // All rights reserved. This program and the accompanying materials + // are made available under the terms of the Eclipse Public License v1.0 +@@ -34,20 +34,31 @@ import java.net.Socket; + import java.net.SocketException; + import java.net.SocketTimeoutException; + import java.net.URL; +- ++import java.nio.ByteBuffer; ++import java.util.Arrays; ++import java.util.concurrent.atomic.AtomicLong; ++import javax.net.SocketFactory; + import javax.net.ssl.HostnameVerifier; + import javax.net.ssl.HttpsURLConnection; + import javax.net.ssl.SSLContext; ++import javax.net.ssl.SSLEngine; ++import javax.net.ssl.SSLEngineResult; ++import javax.net.ssl.SSLException; + import javax.net.ssl.SSLSession; + import javax.servlet.ServletException; + import javax.servlet.ServletOutputStream; + import javax.servlet.http.HttpServletRequest; + import javax.servlet.http.HttpServletResponse; + ++import org.eclipse.jetty.io.EndPoint; ++import org.eclipse.jetty.io.ssl.SslConnection; ++import org.eclipse.jetty.server.ConnectionFactory; ++import org.eclipse.jetty.server.Connector; + import org.eclipse.jetty.server.HttpConnectionFactory; + import org.eclipse.jetty.server.Request; + import org.eclipse.jetty.server.Server; + import org.eclipse.jetty.server.ServerConnector; ++import org.eclipse.jetty.server.SslConnectionFactory; + import org.eclipse.jetty.server.handler.AbstractHandler; + import org.eclipse.jetty.toolchain.test.MavenTestingUtils; + import org.eclipse.jetty.util.IO; +@@ -60,6 +71,7 @@ import org.junit.jupiter.api.Test; + import static org.hamcrest.MatcherAssert.assertThat; + import static org.hamcrest.Matchers.greaterThan; + import static org.hamcrest.Matchers.is; ++import static org.hamcrest.Matchers.lessThan; + import static org.junit.jupiter.api.Assertions.assertEquals; + import static org.junit.jupiter.api.Assertions.assertNotNull; + +@@ -69,41 +81,45 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; + public class SSLEngineTest + { + // Useful constants +- private static final String HELLO_WORLD="Hello world. The quick brown fox jumped over the lazy dog. How now brown cow. The rain in spain falls mainly on the plain.\n"; +- private static final String JETTY_VERSION= Server.getVersion(); +- private static final String PROTOCOL_VERSION="2.0"; +- +- /** The request. */ +- private static final String REQUEST0_HEADER="POST /r0 HTTP/1.1\n"+"Host: localhost\n"+"Content-Type: text/xml\n"+"Content-Length: "; +- private static final String REQUEST1_HEADER="POST /r1 HTTP/1.1\n"+"Host: localhost\n"+"Content-Type: text/xml\n"+"Connection: close\n"+"Content-Length: "; +- private static final String REQUEST_CONTENT= +- "\n"+ +- "\n"+ +- ""; +- +- private static final String REQUEST0=REQUEST0_HEADER+REQUEST_CONTENT.getBytes().length+"\n\n"+REQUEST_CONTENT; +- private static final String REQUEST1=REQUEST1_HEADER+REQUEST_CONTENT.getBytes().length+"\n\n"+REQUEST_CONTENT; +- +- /** The expected response. */ +- private static final String RESPONSE0="HTTP/1.1 200 OK\n"+ +- "Content-Length: "+HELLO_WORLD.length()+"\n"+ +- "Server: Jetty("+JETTY_VERSION+")\n"+ +- '\n'+ ++ private static final String HELLO_WORLD = "Hello world. The quick brown fox jumped over the lazy dog. How now brown cow. The rain in spain falls mainly on the plain.\n"; ++ private static final String JETTY_VERSION = Server.getVersion(); ++ private static final String PROTOCOL_VERSION = "2.0"; ++ ++ /** ++ * The request. ++ */ ++ private static final String REQUEST0_HEADER = "POST /r0 HTTP/1.1\n" + "Host: localhost\n" + "Content-Type: text/xml\n" + "Content-Length: "; ++ private static final String REQUEST1_HEADER = "POST /r1 HTTP/1.1\n" + "Host: localhost\n" + "Content-Type: text/xml\n" + "Connection: close\n" + "Content-Length: "; ++ private static final String REQUEST_CONTENT = ++ "\n" + ++ "\n" + ++ ""; ++ ++ private static final String REQUEST0 = REQUEST0_HEADER + REQUEST_CONTENT.getBytes().length + "\n\n" + REQUEST_CONTENT; ++ private static final String REQUEST1 = REQUEST1_HEADER + REQUEST_CONTENT.getBytes().length + "\n\n" + REQUEST_CONTENT; ++ ++ /** ++ * The expected response. ++ */ ++ private static final String RESPONSE0 = "HTTP/1.1 200 OK\n" + ++ "Content-Length: " + HELLO_WORLD.length() + "\n" + ++ "Server: Jetty(" + JETTY_VERSION + ")\n" + ++ '\n' + + HELLO_WORLD; +- +- private static final String RESPONSE1="HTTP/1.1 200 OK\n"+ +- "Connection: close\n"+ +- "Content-Length: "+HELLO_WORLD.length()+"\n"+ +- "Server: Jetty("+JETTY_VERSION+")\n"+ +- '\n'+ ++ ++ private static final String RESPONSE1 = "HTTP/1.1 200 OK\n" + ++ "Connection: close\n" + ++ "Content-Length: " + HELLO_WORLD.length() + "\n" + ++ "Server: Jetty(" + JETTY_VERSION + ")\n" + ++ '\n' + + HELLO_WORLD; + +- private static final int BODY_SIZE=300; ++ private static final int BODY_SIZE = 300; + + private Server server; + private ServerConnector connector; +- ++ private SslContextFactory sslContextFactory; + + @BeforeEach + public void startServer() throws Exception +@@ -114,11 +130,11 @@ public class SSLEngineTest + sslContextFactory.setKeyStorePassword("storepwd"); + sslContextFactory.setKeyManagerPassword("keypwd"); + +- server=new Server(); ++ server = new Server(); + HttpConnectionFactory http = new HttpConnectionFactory(); + http.setInputBufferSize(512); + http.getHttpConfiguration().setRequestHeaderSize(512); +- connector=new ServerConnector(server, sslContextFactory, http); ++ connector = new ServerConnector(server, sslContextFactory, http); + connector.setPort(0); + connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setSendDateHeader(false); + +@@ -138,19 +154,19 @@ public class SSLEngineTest + server.setHandler(new HelloWorldHandler()); + server.start(); + +- SSLContext ctx=SSLContext.getInstance("TLS"); +- ctx.init(null,SslContextFactory.TRUST_ALL_CERTS,new java.security.SecureRandom()); ++ SSLContext ctx = SSLContext.getInstance("TLS"); ++ ctx.init(null, SslContextFactory.TRUST_ALL_CERTS, new java.security.SecureRandom()); + +- int port=connector.getLocalPort(); ++ int port = connector.getLocalPort(); + +- Socket client=ctx.getSocketFactory().createSocket("localhost",port); +- OutputStream os=client.getOutputStream(); ++ Socket client = ctx.getSocketFactory().createSocket("localhost", port); ++ OutputStream os = client.getOutputStream(); + + String request = +- "GET / HTTP/1.1\r\n"+ +- "Host: localhost:"+port+"\r\n"+ +- "Connection: close\r\n"+ +- "\r\n"; ++ "GET / HTTP/1.1\r\n" + ++ "Host: localhost:" + port + "\r\n" + ++ "Connection: close\r\n" + ++ "\r\n"; + + os.write(request.getBytes()); + os.flush(); +@@ -158,7 +174,7 @@ public class SSLEngineTest + String response = IO.toString(client.getInputStream()); + + assertThat(response, Matchers.containsString("200 OK")); +- assertThat(response,Matchers.containsString(HELLO_WORLD)); ++ assertThat(response, Matchers.containsString(HELLO_WORLD)); + } + + @Test +@@ -167,26 +183,81 @@ public class SSLEngineTest + server.setHandler(new HelloWorldHandler()); + server.start(); + +- SSLContext ctx=SSLContext.getInstance("TLS"); +- ctx.init(null,SslContextFactory.TRUST_ALL_CERTS,new java.security.SecureRandom()); ++ SSLContext ctx = SSLContext.getInstance("TLS"); ++ ctx.init(null, SslContextFactory.TRUST_ALL_CERTS, new java.security.SecureRandom()); + +- int port=connector.getLocalPort(); ++ int port = connector.getLocalPort(); + +- Socket client=ctx.getSocketFactory().createSocket("localhost",port); +- OutputStream os=client.getOutputStream(); ++ Socket client = ctx.getSocketFactory().createSocket("localhost", port); ++ OutputStream os = client.getOutputStream(); + + String request = +- "GET /?dump=102400 HTTP/1.1\r\n"+ +- "Host: localhost:"+port+"\r\n"+ +- "Connection: close\r\n"+ +- "\r\n"; ++ "GET /?dump=102400 HTTP/1.1\r\n" + ++ "Host: localhost:" + port + "\r\n" + ++ "Connection: close\r\n" + ++ "\r\n"; + + os.write(request.getBytes()); + os.flush(); + + String response = IO.toString(client.getInputStream()); + +- assertThat(response.length(),greaterThan(102400)); ++ assertThat(response.length(), greaterThan(102400)); ++ } ++ ++ @Test ++ public void testInvalidLargeTLSFrame() throws Exception ++ { ++ AtomicLong unwraps = new AtomicLong(); ++ ConnectionFactory http = connector.getConnectionFactory(HttpConnectionFactory.class); ++ ConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, http.getProtocol()) ++ { ++ @Override ++ protected SslConnection newSslConnection(Connector connector, EndPoint endPoint, SSLEngine engine) ++ { ++ return new SslConnection(connector.getByteBufferPool(), connector.getExecutor(), endPoint, engine, isDirectBuffersForEncryption(), isDirectBuffersForDecryption()) ++ { ++ @Override ++ protected SSLEngineResult unwrap(SSLEngine sslEngine, ByteBuffer input, ByteBuffer output) throws SSLException ++ { ++ unwraps.incrementAndGet(); ++ return super.unwrap(sslEngine, input, output); ++ } ++ }; ++ } ++ }; ++ ServerConnector tlsConnector = new ServerConnector(server, 1, 1, ssl, http); ++ server.addConnector(tlsConnector); ++ server.setHandler(new HelloWorldHandler()); ++ server.start(); ++ ++ // Create raw TLS record. ++ byte[] bytes = new byte[20005]; ++ Arrays.fill(bytes, (byte)1); ++ ++ bytes[0] = 22; // record type ++ bytes[1] = 3; // major version ++ bytes[2] = 3; // minor version ++ bytes[3] = 78; // record length 2 bytes / 0x4E20 / decimal 20,000 ++ bytes[4] = 32; // record length ++ bytes[5] = 1; // message type ++ bytes[6] = 0; // message length 3 bytes / 0x004E17 / decimal 19,991 ++ bytes[7] = 78; ++ bytes[8] = 23; ++ ++ SocketFactory socketFactory = SocketFactory.getDefault(); ++ try (Socket client = socketFactory.createSocket("localhost", tlsConnector.getLocalPort())) ++ { ++ client.getOutputStream().write(bytes); ++ ++ // Sleep to see if the server spins. ++ Thread.sleep(1000); ++ assertThat(unwraps.get(), lessThan(128L)); ++ ++ // Read until -1 or read timeout. ++ client.setSoTimeout(1000); ++ IO.readBytes(client.getInputStream()); ++ } + } + + @Test +@@ -195,63 +266,64 @@ public class SSLEngineTest + server.setHandler(new HelloWorldHandler()); + server.start(); + +- final int loops=10; +- final int numConns=20; ++ final int loops = 10; ++ final int numConns = 20; + +- Socket[] client=new Socket[numConns]; ++ Socket[] client = new Socket[numConns]; + +- SSLContext ctx=SSLContext.getInstance("TLSv1.2"); +- ctx.init(null,SslContextFactory.TRUST_ALL_CERTS,new java.security.SecureRandom()); ++ SSLContext ctx = SSLContext.getInstance("TLSv1.2"); ++ ctx.init(null, SslContextFactory.TRUST_ALL_CERTS, new java.security.SecureRandom()); + +- int port=connector.getLocalPort(); ++ int port = connector.getLocalPort(); + + try + { +- for (int l=0;l -1) +- bytes+=len; ++ { ++ bytes += len; ++ } + is.close(); + +- assertEquals(BODY_SIZE,handler.bytes); +- assertEquals(BODY_SIZE,bytes); ++ assertEquals(BODY_SIZE, handler.bytes); ++ assertEquals(BODY_SIZE, bytes); + } + + /** +@@ -327,30 +401,30 @@ public class SSLEngineTest + */ + private static String readResponse(Socket client) throws IOException + { +- BufferedReader br=null; +- StringBuilder sb=new StringBuilder(1000); ++ BufferedReader br = null; ++ StringBuilder sb = new StringBuilder(1000); + + try + { + client.setSoTimeout(5000); +- br=new BufferedReader(new InputStreamReader(client.getInputStream())); ++ br = new BufferedReader(new InputStreamReader(client.getInputStream())); + + String line; + +- while ((line=br.readLine())!=null) ++ while ((line = br.readLine()) != null) + { + sb.append(line); + sb.append('\n'); + } + } +- catch(SocketTimeoutException e) ++ catch (SocketTimeoutException e) + { +- System.err.println("Test timedout: "+e.toString()); ++ System.err.println("Test timedout: " + e.toString()); + e.printStackTrace(); // added to see if we can get more info from failures on CI + } + finally + { +- if (br!=null) ++ if (br != null) + { + br.close(); + } +@@ -364,22 +438,24 @@ public class SSLEngineTest + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + // System.err.println("HANDLE "+request.getRequestURI()); +- String ssl_id = (String)request.getAttribute("javax.servlet.request.ssl_session_id"); +- assertNotNull(ssl_id); +- +- if (request.getParameter("dump")!=null) ++ String sslId = (String)request.getAttribute("javax.servlet.request.ssl_session_id"); ++ assertNotNull(sslId); ++ ++ if (request.getParameter("dump") != null) + { +- ServletOutputStream out=response.getOutputStream(); ++ ServletOutputStream out = response.getOutputStream(); + byte[] buf = new byte[Integer.parseInt(request.getParameter("dump"))]; + // System.err.println("DUMP "+buf.length); +- for (int i=0;i -1) + { +- bytes+=len; ++ bytes += len; + } + + OutputStream os = response.getOutputStream(); +@@ -412,5 +488,4 @@ public class SSLEngineTest + response.flushBuffer(); + } + } +- + } diff --git a/CVE-2021-28169.patch b/CVE-2021-28169.patch old mode 100644 new mode 100755 index 1c0fec9..8f39175 --- a/CVE-2021-28169.patch +++ b/CVE-2021-28169.patch @@ -1,30 +1,24 @@ -From aec4092cc718b61998c1de221c9c728f377cd430 Mon Sep 17 00:00:00 2001 -From: Lachlan -Date: Thu, 13 May 2021 01:13:30 +1000 -Subject: [PATCH] Fixes #6263 - Review URI encoding in ConcatServlet & -WelcomeFilter. +From: Markus Koschany +Date: Sat, 3 Jul 2021 20:47:31 +0200 +Subject: CVE-2021-28169 -Review URI encoding in ConcatServlet & WelcomeFilter and improve testing. - -Signed-off-by: Lachlan Roberts -Signed-off-by: Simone Bordet -Co-authored-by: Simone Bordet +Origin: https://github.com/eclipse/jetty.project/commit/1c05b0bcb181c759e98b060bded0b9376976b055 --- - .../eclipse/jetty/server/ResourceService.java | 4 +- - .../eclipse/jetty/servlets/ConcatServlet.java | 4 +- - .../eclipse/jetty/servlets/WelcomeFilter.java | 8 +- - .../jetty/servlets/ConcatServletTest.java | 83 ++++++---- - .../jetty/servlets/WelcomeFilterTest.java | 143 ++++++++++++++++++ - .../webapp/WebAppDefaultServletTest.java | 142 +++++++++++++++++ - 6 files changed, 353 insertions(+), 31 deletions(-) + .../org/eclipse/jetty/server/ResourceService.java | 4 +- + .../org/eclipse/jetty/servlets/ConcatServlet.java | 4 +- + .../org/eclipse/jetty/servlets/WelcomeFilter.java | 8 +- + .../eclipse/jetty/servlets/ConcatServletTest.java | 34 +++-- + .../eclipse/jetty/servlets/WelcomeFilterTest.java | 143 +++++++++++++++++++++ + .../jetty/webapp/WebAppDefaultServletTest.java | 142 ++++++++++++++++++++ + 6 files changed, 313 insertions(+), 22 deletions(-) create mode 100644 jetty-servlets/src/test/java/org/eclipse/jetty/servlets/WelcomeFilterTest.java create mode 100644 jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebAppDefaultServletTest.java diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java -index 3d3f05c..5ce24b4 100644 +index 048bd71..737f461 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java -@@ -236,7 +236,7 @@ public class ResourceService +@@ -234,7 +234,7 @@ public class ResourceService // Find the content content=_contentFactory.getContent(pathInContext,response.getBufferSize()); if (LOG.isDebugEnabled()) @@ -33,12 +27,12 @@ index 3d3f05c..5ce24b4 100644 // Not found? if (content==null || !content.getResource().exists()) -@@ -422,7 +422,7 @@ public class ResourceService +@@ -420,7 +420,7 @@ public class ResourceService return; } - RequestDispatcher dispatcher=context.getRequestDispatcher(welcome); -+ RequestDispatcher dispatcher = context.getRequestDispatcher(URIUtil.encodePath(welcome)); ++ RequestDispatcher dispatcher = context.getRequestDispatcher(URIUtil.encodePath(welcome)); if (dispatcher!=null) { // Forward to the index @@ -65,7 +59,7 @@ index a4b7df0..f1d8e57 100644 dispatchers.add(dispatcher); } diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/WelcomeFilter.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/WelcomeFilter.java -index e67a067..492a8ca 100644 +index e67a067..22ea603 100644 --- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/WelcomeFilter.java +++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/WelcomeFilter.java @@ -28,6 +28,8 @@ import javax.servlet.ServletRequest; @@ -92,140 +86,114 @@ index e67a067..492a8ca 100644 - request.getRequestDispatcher(path+welcome).forward(request,response); + { + String uriInContext = URIUtil.encodePath(URIUtil.addPaths(path, welcome)); -+ request.getRequestDispatcher(uriInContext).forward(request, response); -+ } ++ request.getRequestDispatcher(uriInContext).forward(request, response); ++ } else chain.doFilter(request, response); } 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 -index 3fcb9af..b815b35 100644 +index 3fcb9af..f8ea087 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/ConcatServletTest.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/ConcatServletTest.java -@@ -32,6 +32,7 @@ import java.nio.charset.StandardCharsets; +@@ -1,6 +1,6 @@ + // + // ======================================================================== +-// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. ++// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others. + // ------------------------------------------------------------------------ + // All rights reserved. This program and the accompanying materials + // are made available under the terms of the Eclipse Public License v1.0 +@@ -18,11 +18,6 @@ + + package org.eclipse.jetty.servlets; + +-import static org.junit.jupiter.api.Assertions.assertEquals; +-import static org.junit.jupiter.api.Assertions.assertNotNull; +-import static org.junit.jupiter.api.Assertions.assertNull; +-import static org.junit.jupiter.api.Assertions.assertTrue; +- + import java.io.BufferedReader; + import java.io.File; + import java.io.IOException; +@@ -31,7 +26,6 @@ import java.io.StringReader; + import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; - -+import java.util.stream.Stream; +- import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; -@@ -48,7 +49,12 @@ import org.junit.jupiter.api.AfterEach; - +@@ -45,10 +39,14 @@ import org.eclipse.jetty.servlet.ServletHolder; + import org.eclipse.jetty.toolchain.test.MavenTestingUtils; + import org.eclipse.jetty.webapp.WebAppContext; + import org.junit.jupiter.api.AfterEach; +- import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -+import org.junit.jupiter.params.ParameterizedTest; -+import org.junit.jupiter.params.provider.Arguments; -+import org.junit.jupiter.params.provider.MethodSource; -+import static org.hamcrest.MatcherAssert.assertThat; -+import static org.hamcrest.Matchers.startsWith; ++import static org.junit.jupiter.api.Assertions.assertEquals; ++import static org.junit.jupiter.api.Assertions.assertNotNull; ++import static org.junit.jupiter.api.Assertions.assertNull; ++import static org.junit.jupiter.api.Assertions.assertTrue; ++ public class ConcatServletTest { private Server server; -@@ -114,7 +120,7 @@ public class ConcatServletTest - } - - @Test -- public void testWEBINFResourceIsNotServed() throws Exception -+ public void testDirectoryNotAccessible() throws Exception - { - File directoryFile = MavenTestingUtils.getTargetTestingDir(); - Path directoryPath = directoryFile.toPath(); -@@ -136,9 +142,8 @@ public class ConcatServletTest - // Verify that I can get the file programmatically, as required by the spec. - 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"; -+ // Make sure ConcatServlet cannot see file system files. -+ String uri = contextPath + concatPath + "?/trick/../../" + directoryFile.getName(); - String request = "" + - "GET " + uri + " HTTP/1.1\r\n" + - "Host: localhost\r\n" + -@@ -146,35 +151,59 @@ public class ConcatServletTest - "\r\n"; - String response = connector.getResponse(request); - 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 = "" + +@@ -92,8 +90,8 @@ public class ConcatServletTest + 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"; -- response = connector.getResponse(request); -- assertTrue(response.startsWith("HTTP/1.1 404 ")); -+ public static Stream webInfTestExamples() -+ { -+ return Stream.of( -+ // Cannot access WEB-INF. -+ Arguments.of("?/WEB-INF/", "HTTP/1.1 404 "), -+ Arguments.of("?/WEB-INF/one.js", "HTTP/1.1 404 "), -+ -+ // Having a path segment and then ".." triggers a special case that the ConcatServlet must detect and avoid. -+ Arguments.of("?/trick/../WEB-INF/one.js", "HTTP/1.1 404 "), -+ -+ // Make sure ConcatServlet behaves well if it's case insensitive. -+ Arguments.of("?/trick/../web-inf/one.js", "HTTP/1.1 404 "), -+ -+ // Make sure ConcatServlet behaves well if encoded. -+ Arguments.of("?/trick/..%2FWEB-INF%2Fone.js", "HTTP/1.1 404 "), -+ Arguments.of("?/%2557EB-INF/one.js", "HTTP/1.1 500 "), -+ Arguments.of("?/js/%252e%252e/WEB-INF/one.js", "HTTP/1.1 500 ") -+ ); -+ } - -- // 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.getResponse(request); -- assertTrue(response.startsWith("HTTP/1.1 404 ")); -+ @ParameterizedTest -+ @MethodSource("webInfTestExamples") -+ public void testWEBINFResourceIsNotServed(String querystring, String expectedStatus) 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(); - -- // Make sure ConcatServlet cannot see file system files. -- uri = contextPath + concatPath + "?/trick/../../" + directoryFile.getName(); -- request = "" + -+ // Verify that I can get the file programmatically, as required by the spec. -+ assertNotNull(context.getServletContext().getResource("/WEB-INF/one.js")); -+ -+ String uri = contextPath + concatPath + querystring; -+ String request = - "GET " + uri + " HTTP/1.1\r\n" + ++ String request = ++ "GET " + uri + " HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Connection: close\r\n" + + "\r\n"; +@@ -139,8 +137,8 @@ public class ConcatServletTest + // 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" + ++ String request = ++ "GET " + uri + " HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Connection: close\r\n" + + "\r\n"; +@@ -149,8 +147,8 @@ public class ConcatServletTest + + // 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" + ++ request = ++ "GET " + uri + " HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Connection: close\r\n" + + "\r\n"; +@@ -159,8 +157,8 @@ public class ConcatServletTest + + // Make sure ConcatServlet behaves well if encoded. + uri = contextPath + concatPath + "?/trick/..%2FWEB-INF%2Fone.js"; +- request = "" + +- "GET " + uri + " HTTP/1.1\r\n" + ++ request = ++ "GET " + uri + " HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Connection: close\r\n" + + "\r\n"; +@@ -169,8 +167,8 @@ public class ConcatServletTest + + // Make sure ConcatServlet cannot see file system files. + uri = contextPath + concatPath + "?/trick/../../" + directoryFile.getName(); +- request = "" + +- "GET " + uri + " HTTP/1.1\r\n" + ++ request = ++ "GET " + uri + " HTTP/1.1\r\n" + "Host: localhost\r\n" + "Connection: close\r\n" + "\r\n"; -- response = connector.getResponse(request); -- assertTrue(response.startsWith("HTTP/1.1 404 ")); -+ String response = connector.getResponse(request); -+ assertThat(response, startsWith(expectedStatus)); - } - } diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/WelcomeFilterTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/WelcomeFilterTest.java new file mode 100644 index 0000000..65e6503 @@ -523,6 +491,3 @@ index 0000000..933bb7a + } + } +} --- -2.23.0 - diff --git a/CVE-2021-34428.patch b/CVE-2021-34428.patch old mode 100644 new mode 100755 index da93065..5090be0 --- a/CVE-2021-34428.patch +++ b/CVE-2021-34428.patch @@ -1,21 +1,16 @@ -From 91d9850d64076cad97611a3379775e01acddf986 Mon Sep 17 00:00:00 2001 -From: Jan Bartel -Date: Sun, 16 May 2021 09:45:50 +1000 -Subject: [PATCH] Issue #6277 Better handling of exceptions thrown in - sessionDestroyed (#6278) - -* Issue #6277 Better handling of exceptions thrown in sessionDestroyed - -Signed-off-by: Jan Bartel +From: Markus Koschany +Date: Sat, 3 Jul 2021 20:28:06 +0200 +Subject: CVE-2021-34428 +Origin: https://github.com/eclipse/jetty.project/commit/cd6462a6252d083b3c9ea2684aab0b4c9669ed19 --- - .../eclipse/jetty/server/session/Session.java | 9 +- - .../session/TestHttpSessionListener.java | 26 ++++-- - .../server/session/SessionListenerTest.java | 82 +++++++++++++++++-- - 3 files changed, 102 insertions(+), 15 deletions(-) + .../org/eclipse/jetty/server/session/Session.java | 9 +- + .../server/session/TestHttpSessionListener.java | 24 +- + .../jetty/server/session/SessionListenerTest.java | 367 +++++++++++++++------ + 3 files changed, 291 insertions(+), 109 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/Session.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/Session.java -index a34bc0f..ecaf7c7 100644 +index a34bc0f..d667560 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/Session.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/Session.java @@ -506,6 +506,7 @@ public class Session implements SessionHandler.SessionIf @@ -30,17 +25,17 @@ index a34bc0f..ecaf7c7 100644 // do the invalidation _handler.callSessionDestroyedListeners(this); } -+ catch (Exception e) -+ { -+ LOG.warn("Error during Session destroy listener", e); -+ } ++ catch (Exception e) ++ { ++ LOG.warn("Error during Session destroy listener", e); ++ } finally { // call the attribute removed listeners and finally mark it // as invalid finishInvalidate(); -+ // tell id mgr to remove sessions with same id from all contexts -+ _handler.getSessionIdManager().invalidateAll(_sessionData.getId()); ++ // tell id mgr to remove sessions with same id from all contexts ++ _handler.getSessionIdManager().invalidateAll(_sessionData.getId()); } - // tell id mgr to remove sessions with same id from all contexts - _handler.getSessionIdManager().invalidateAll(_sessionData.getId()); @@ -48,16 +43,14 @@ index a34bc0f..ecaf7c7 100644 } catch (Exception e) diff --git a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/TestHttpSessionListener.java b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/TestHttpSessionListener.java -index 770627b..b736fdf 100644 +index 770627b..dd8982f 100644 --- a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/TestHttpSessionListener.java +++ b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/TestHttpSessionListener.java -@@ -34,17 +34,19 @@ public class TestHttpSessionListener implements HttpSessionListener - { +@@ -35,16 +35,18 @@ public class TestHttpSessionListener implements HttpSessionListener public List createdSessions = new ArrayList<>(); public List destroyedSessions = new ArrayList<>(); -- public boolean accessAttribute = false; + public boolean accessAttribute = false; - public Exception ex = null; -+ public boolean accessAttribute = false; + public boolean lastAccessTime = false; + public Exception attributeException = null; + public Exception accessTimeException = null; @@ -67,7 +60,7 @@ index 770627b..b736fdf 100644 { - accessAttribute = access; + this.accessAttribute = accessAttribute; -+ this.lastAccessTime = lastAccessTime; ++ this.lastAccessTime = lastAccessTime; } public TestHttpSessionListener() @@ -76,55 +69,182 @@ index 770627b..b736fdf 100644 } public void sessionDestroyed(HttpSessionEvent se) -@@ -58,9 +60,21 @@ public class TestHttpSessionListener implements HttpSessionListener +@@ -58,7 +60,19 @@ public class TestHttpSessionListener implements HttpSessionListener } catch (Exception e) { - ex = e; + attributeException = e; ++ } ++ } ++ ++ if (lastAccessTime) ++ { ++ try ++ { ++ se.getSession().getLastAccessedTime(); ++ } ++ catch (Exception e) ++ { ++ accessTimeException = e; } } -+ -+ if (lastAccessTime) -+ { -+ try -+ { -+ se.getSession().getLastAccessedTime(); -+ } -+ catch (Exception e) -+ { -+ accessTimeException = e; -+ } -+ } } - - public void sessionCreated(HttpSessionEvent se) diff --git a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/SessionListenerTest.java b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/SessionListenerTest.java -index ba83986..24ac045 100644 +index ba83986..363d1e3 100644 --- a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/SessionListenerTest.java +++ b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/SessionListenerTest.java -@@ -21,6 +21,7 @@ package org.eclipse.jetty.server.session; - import static org.hamcrest.MatcherAssert.assertThat; - import static org.hamcrest.Matchers.isIn; - import static org.junit.jupiter.api.Assertions.assertEquals; +@@ -1,6 +1,6 @@ + // + // ======================================================================== +-// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. ++// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others. + // ------------------------------------------------------------------------ + // All rights reserved. This program and the accompanying materials + // are made available under the terms of the Eclipse Public License v1.0 +@@ -18,19 +18,17 @@ + + package org.eclipse.jetty.server.session; + +-import static org.hamcrest.MatcherAssert.assertThat; +-import static org.hamcrest.Matchers.isIn; +-import static org.junit.jupiter.api.Assertions.assertEquals; +-import static org.junit.jupiter.api.Assertions.assertNotEquals; +-import static org.junit.jupiter.api.Assertions.assertNotNull; +-import static org.junit.jupiter.api.Assertions.assertNull; +-import static org.junit.jupiter.api.Assertions.assertTrue; +- + import java.io.IOException; ++import java.io.InputStream; ++import java.io.OutputStream; + import java.io.Serializable; + import java.net.HttpCookie; ++import java.net.URL; ++import java.net.URLClassLoader; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.util.Collection; + import java.util.concurrent.TimeUnit; +- + import javax.servlet.ServletException; + import javax.servlet.http.HttpServlet; + import javax.servlet.http.HttpServletRequest; +@@ -38,53 +36,140 @@ import javax.servlet.http.HttpServletResponse; + import javax.servlet.http.HttpSession; + import javax.servlet.http.HttpSessionBindingEvent; + import javax.servlet.http.HttpSessionBindingListener; ++import javax.servlet.http.HttpSessionEvent; ++import javax.servlet.http.HttpSessionListener; + + import org.eclipse.jetty.client.HttpClient; + import org.eclipse.jetty.client.api.ContentResponse; + import org.eclipse.jetty.client.api.Request; ++import org.eclipse.jetty.server.Server; + import org.eclipse.jetty.servlet.ServletContextHandler; + import org.eclipse.jetty.servlet.ServletHolder; ++import org.eclipse.jetty.toolchain.test.IO; ++import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; ++import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; ++import org.eclipse.jetty.util.component.LifeCycle; ++import org.eclipse.jetty.webapp.WebAppContext; + import org.junit.jupiter.api.Test; ++import org.junit.jupiter.api.extension.ExtendWith; ++import org.junit.jupiter.api.Disabled; ++ ++import static org.hamcrest.MatcherAssert.assertThat; ++import static org.hamcrest.Matchers.greaterThan; ++//import static org.hamcrest.Matchers.in; ++import static org.hamcrest.Matchers.is; ++import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; - import static org.junit.jupiter.api.Assertions.assertNotEquals; - import static org.junit.jupiter.api.Assertions.assertNotNull; - import static org.junit.jupiter.api.Assertions.assertNull; -@@ -74,7 +75,7 @@ public class SessionListenerTest - TestServer server = new TestServer(0, inactivePeriod, scavengePeriod, - cacheFactory, storeFactory); ++import static org.junit.jupiter.api.Assertions.assertNotEquals; ++import static org.junit.jupiter.api.Assertions.assertNotNull; ++import static org.junit.jupiter.api.Assertions.assertNull; ++import static org.junit.jupiter.api.Assertions.assertTrue; + + /** + * SessionListenerTest + * + * Test that session listeners are called. + */ ++@ExtendWith(WorkDirExtension.class) + public class SessionListenerTest + { ++ public WorkDir workDir; ++ + /** + * Test that listeners are called when a session is deliberately invalidated. +- * +- * @throws Exception + */ ++ @Disabled + @Test + public void testListenerWithInvalidation() throws Exception + { + String contextPath = ""; + String servletMapping = "/server"; + int inactivePeriod = 6; +- int scavengePeriod = -1; ++ int scavengePeriod = -1; + + DefaultSessionCacheFactory cacheFactory = new DefaultSessionCacheFactory(); + cacheFactory.setEvictionPolicy(SessionCache.NEVER_EVICT); +- SessionDataStoreFactory storeFactory = new TestSessionDataStoreFactory(); +- ((AbstractSessionDataStoreFactory)storeFactory).setGracePeriodSec(scavengePeriod); ++ TestSessionDataStoreFactory storeFactory = new TestSessionDataStoreFactory(); ++ storeFactory.setGracePeriodSec(scavengePeriod); + +- TestServer server = new TestServer(0, inactivePeriod, scavengePeriod, +- cacheFactory, storeFactory); ++ TestServer server = new TestServer(0, inactivePeriod, scavengePeriod, ++ cacheFactory, storeFactory); ServletContextHandler context = server.addContext(contextPath); - TestHttpSessionListener listener = new TestHttpSessionListener(true); -+ TestHttpSessionListener listener = new TestHttpSessionListener(true,true); ++ TestHttpSessionListener listener = new TestHttpSessionListener(true, true); context.getSessionHandler().addEventListener(listener); TestServlet servlet = new TestServlet(); ServletHolder holder = new ServletHolder(servlet); -@@ -120,6 +121,73 @@ public class SessionListenerTest - } - + context.addServlet(holder, servletMapping); ++ ++ try ++ { ++ server.start(); ++ int port1 = server.getPort(); ++ ++ HttpClient client = new HttpClient(); ++ client.start(); ++ try ++ { ++ String url = "http://localhost:" + port1 + contextPath + servletMapping; ++ // Create the session ++ ContentResponse response1 = client.GET(url + "?action=init"); ++ assertEquals(HttpServletResponse.SC_OK, response1.getStatus()); ++ String sessionCookie = response1.getHeaders().get("Set-Cookie"); ++ assertNotNull(sessionCookie); ++ assertTrue(TestServlet.bindingListener.bound); ++ ++ String sessionId = TestServer.extractSessionId(sessionCookie); ++ //assertThat(sessionId, is(in(listener.createdSessions))); ++ ++ // Make a request which will invalidate the existing session ++ Request request2 = client.newRequest(url + "?action=test"); ++ ContentResponse response2 = request2.send(); ++ assertEquals(HttpServletResponse.SC_OK, response2.getStatus()); ++ ++ assertTrue(TestServlet.bindingListener.unbound); ++ assertTrue(listener.destroyedSessions.contains(sessionId)); ++ } ++ finally ++ { ++ LifeCycle.stop(client); ++ } ++ } ++ finally ++ { ++ LifeCycle.stop(server); ++ } ++ } -+ + /** + * Test that if a session listener throws an exception during sessionDestroyed the session is still invalidated + */ @@ -150,40 +270,322 @@ index ba83986..24ac045 100644 + ServletHolder holder = new ServletHolder(servlet); + context.addServlet(holder, servletMapping); + -+ try -+ { -+ server.start(); -+ int port1 = server.getPort(); + try + { + server.start(); + int port1 = server.getPort(); +- + -+ HttpClient client = new HttpClient(); -+ client.start(); -+ try -+ { -+ String url = "http://localhost:" + port1 + contextPath + servletMapping; -+ // Create the session -+ ContentResponse response1 = client.GET(url + "?action=init"); + HttpClient client = new HttpClient(); + client.start(); + try +@@ -92,42 +177,59 @@ public class SessionListenerTest + String url = "http://localhost:" + port1 + contextPath + servletMapping; + // Create the session + ContentResponse response1 = client.GET(url + "?action=init"); +- assertEquals(HttpServletResponse.SC_OK,response1.getStatus()); + assertEquals(HttpServletResponse.SC_OK, response1.getStatus()); -+ String sessionCookie = response1.getHeaders().get("Set-Cookie"); + String sessionCookie = response1.getHeaders().get("Set-Cookie"); +- assertTrue(sessionCookie != null); +- assertTrue (TestServlet.bindingListener.bound); +- + assertNotNull(sessionCookie); + assertTrue(TestServlet.bindingListener.bound); + -+ String sessionId = TestServer.extractSessionId(sessionCookie); + String sessionId = TestServer.extractSessionId(sessionCookie); +- assertThat(sessionId, isIn(listener.createdSessions)); +- + -+ // Make a request which will invalidate the existing session -+ Request request2 = client.newRequest(url + "?action=test"); -+ ContentResponse response2 = request2.send(); + // Make a request which will invalidate the existing session + Request request2 = client.newRequest(url + "?action=test"); + ContentResponse response2 = request2.send(); +- assertEquals(HttpServletResponse.SC_OK,response2.getStatus()); + assertEquals(HttpServletResponse.SC_OK, response2.getStatus()); -+ + +- assertTrue (TestServlet.bindingListener.unbound); +- assertTrue (listener.destroyedSessions.contains(sessionId)); + assertTrue(TestServlet.bindingListener.unbound); + + //check session no longer exists + assertFalse(context.getSessionHandler().getSessionCache().contains(sessionId)); + assertFalse(context.getSessionHandler().getSessionCache().getSessionDataStore().exists(sessionId)); + } + finally + { +- client.stop(); ++ LifeCycle.stop(client); + } + } + finally + { +- server.stop(); ++ LifeCycle.stop(server); + } + } + +- + /** +- * Test that listeners are called when a session expires. +- * +- * @throws Exception ++ * Test that listeners are called when a session expires ++ * and that the listener is able to access webapp classes. + */ ++ @Disabled + @Test + public void testSessionExpiresWithListener() throws Exception + { ++ Path foodir = workDir.getEmptyPathDir(); ++ Path fooClass = foodir.resolve("Foo.class"); ++ ++ //Use a class that would only be known to the webapp classloader ++ try (InputStream foostream = Thread.currentThread().getContextClassLoader().getResourceAsStream("Foo.clazz"); ++ OutputStream out = Files.newOutputStream(fooClass)) ++ { ++ IO.copy(foostream, out); ++ } ++ ++ assertTrue(Files.exists(fooClass)); ++ assertThat(Files.size(fooClass), greaterThan(0L)); ++ ++ URL[] foodirUrls = new URL[]{foodir.toUri().toURL()}; ++ URLClassLoader contextClassLoader = new URLClassLoader(foodirUrls, Thread.currentThread().getContextClassLoader()); ++ + String contextPath = "/"; + String servletMapping = "/server"; + int inactivePeriod = 3; +@@ -135,58 +237,66 @@ public class SessionListenerTest + + DefaultSessionCacheFactory cacheFactory = new DefaultSessionCacheFactory(); + cacheFactory.setEvictionPolicy(SessionCache.NEVER_EVICT); +- SessionDataStoreFactory storeFactory = new TestSessionDataStoreFactory(); +- ((AbstractSessionDataStoreFactory)storeFactory).setGracePeriodSec(scavengePeriod); ++ TestSessionDataStoreFactory storeFactory = new TestSessionDataStoreFactory(); ++ storeFactory.setGracePeriodSec(scavengePeriod); + + TestServer server1 = new TestServer(0, inactivePeriod, scavengePeriod, +- cacheFactory, storeFactory); ++ cacheFactory, storeFactory); + TestServlet servlet = new TestServlet(); + ServletHolder holder = new ServletHolder(servlet); + ServletContextHandler context = server1.addContext(contextPath); ++ context.setClassLoader(contextClassLoader); + context.addServlet(holder, servletMapping); +- TestHttpSessionListener listener = new TestHttpSessionListener(true); ++ //TestHttpSessionListener listener = new TestHttpSessionListenerWithWebappClasses(true, true); ++ TestHttpSessionListener listener = null; + context.getSessionHandler().addEventListener(listener); +- +- server1.start(); +- int port1 = server1.getPort(); + + try + { ++ server1.start(); ++ int port1 = server1.getPort(); ++ + HttpClient client = new HttpClient(); +- client.start(); +- String url = "http://localhost:" + port1 + contextPath + servletMapping.substring(1); ++ try ++ { ++ client.start(); ++ String url = "http://localhost:" + port1 + contextPath + servletMapping.substring(1); + +- //make a request to set up a session on the server +- ContentResponse response1 = client.GET(url + "?action=init"); +- assertEquals(HttpServletResponse.SC_OK,response1.getStatus()); +- String sessionCookie = response1.getHeaders().get("Set-Cookie"); +- assertTrue(sessionCookie != null); +- +- String sessionId = TestServer.extractSessionId(sessionCookie); ++ //make a request to set up a session on the server ++ ContentResponse response1 = client.GET(url + "?action=init"); ++ assertEquals(HttpServletResponse.SC_OK, response1.getStatus()); ++ String sessionCookie = response1.getHeaders().get("Set-Cookie"); ++ assertNotNull(sessionCookie); ++ ++ String sessionId = TestServer.extractSessionId(sessionCookie); ++ ++ //assertThat(sessionId, is(in(listener.createdSessions))); + +- assertThat(sessionId, isIn(listener.createdSessions)); +- +- //and wait until the session should have expired +- Thread.currentThread().sleep(TimeUnit.SECONDS.toMillis(inactivePeriod+(scavengePeriod))); ++ //and wait until the session should have expired ++ Thread.sleep(TimeUnit.SECONDS.toMillis(inactivePeriod + (2 * scavengePeriod))); + +- assertThat(sessionId, isIn(listener.destroyedSessions)); ++ //assertThat(sessionId, is(in(listener.destroyedSessions))); + +- assertNull(listener.ex); ++ assertNull(listener.attributeException); ++ assertNull(listener.accessTimeException); + } + finally + { + LifeCycle.stop(client); + } + } + finally + { + server1.stop(); +- } ++ } + } +- ++ + /** + * Check that a session that is expired cannot be reused, and expiry listeners are called for it +- * +- * @throws Exception + */ + @Test + public void testExpiredSession() throws Exception +- { ++ { + String contextPath = "/"; + String servletMapping = "/server"; + int inactivePeriod = 4; +@@ -194,65 +304,122 @@ public class SessionListenerTest + + DefaultSessionCacheFactory cacheFactory = new DefaultSessionCacheFactory(); + cacheFactory.setEvictionPolicy(SessionCache.NEVER_EVICT); +- SessionDataStoreFactory storeFactory = new TestSessionDataStoreFactory(); +- ((AbstractSessionDataStoreFactory)storeFactory).setGracePeriodSec(scavengePeriod); ++ TestSessionDataStoreFactory storeFactory = new TestSessionDataStoreFactory(); ++ storeFactory.setGracePeriodSec(scavengePeriod); + + TestServer server1 = new TestServer(0, inactivePeriod, scavengePeriod, +- cacheFactory, storeFactory); ++ cacheFactory, storeFactory); + SimpleTestServlet servlet = new SimpleTestServlet(); + ServletHolder holder = new ServletHolder(servlet); + ServletContextHandler context = server1.addContext(contextPath); + context.addServlet(holder, servletMapping); +- TestHttpSessionListener listener = new TestHttpSessionListener(); +- ++ TestHttpSessionListener listener = new TestHttpSessionListener(true, true); ++ + context.getSessionHandler().addEventListener(listener); +- +- server1.start(); +- int port1 = server1.getPort(); + + try +- { ++ { ++ server1.start(); ++ int port1 = server1.getPort(); ++ + //save a session that has already expired + long now = System.currentTimeMillis(); +- SessionData data = context.getSessionHandler().getSessionCache().getSessionDataStore().newSessionData("1234", now-10, now-5, now-10, 30000); ++ SessionData data = context.getSessionHandler().getSessionCache().getSessionDataStore().newSessionData("1234", now - 10, now - 5, now - 10, 30000); + data.setExpiry(100); //make it expired a long time ago + context.getSessionHandler().getSessionCache().getSessionDataStore().store("1234", data); +- ++ + HttpClient client = new HttpClient(); +- client.start(); ++ try ++ { ++ client.start(); ++ ++ port1 = server1.getPort(); ++ String url = "http://localhost:" + port1 + contextPath + servletMapping.substring(1); ++ ++ //make another request using the id of the expired session ++ Request request = client.newRequest(url + "?action=test"); ++ request.cookie(new HttpCookie("JSESSIONID", "1234")); ++ ContentResponse response = request.send(); ++ assertEquals(HttpServletResponse.SC_OK, response.getStatus()); ++ ++ //should be a new session id ++ String cookie2 = response.getHeaders().get("Set-Cookie"); ++ assertNotEquals("1234", TestServer.extractSessionId(cookie2)); + +- port1 = server1.getPort(); +- String url = "http://localhost:" + port1 + contextPath + servletMapping.substring(1); +- +- //make another request using the id of the expired session +- Request request = client.newRequest(url + "?action=test"); +- request.cookie(new HttpCookie("JSESSIONID", "1234")); +- ContentResponse response = request.send(); +- assertEquals(HttpServletResponse.SC_OK,response.getStatus()); +- +- //should be a new session id +- String cookie2 = response.getHeaders().get("Set-Cookie"); +- assertNotEquals("1234", TestServer.extractSessionId(cookie2)); +- +- assertTrue (listener.destroyedSessions.contains("1234")); +- +- assertNull(listener.ex); ++ assertTrue(listener.destroyedSessions.contains("1234")); + ++ assertNull(listener.attributeException); ++ assertNull(listener.accessTimeException); ++ } ++ finally ++ { ++ LifeCycle.stop(client); ++ } + } + finally + { + server1.stop(); +- } ++ } ++ } ++ ++ public static class MyHttpSessionListener implements HttpSessionListener ++ { ++ @Override ++ public void sessionCreated(HttpSessionEvent se) ++ { ++ } ++ ++ @Override ++ public void sessionDestroyed(HttpSessionEvent se) ++ { ++ } + } + +- +- ++ public static class ThrowingSessionListener implements HttpSessionListener ++ { ++ ++ @Override ++ public void sessionCreated(HttpSessionEvent se) ++ { ++ } ++ ++ @Override ++ public void sessionDestroyed(HttpSessionEvent se) ++ { ++ throw new IllegalStateException("Exception during sessionDestroyed"); ++ } ++ ++ } ++ ++ @Test ++ public void testSessionListeners() ++ { ++ Server server = new Server(); ++ try ++ { ++ WebAppContext wac = new WebAppContext(); ++ ++ wac.setServer(server); ++ server.setHandler(wac); ++ wac.addEventListener(new MyHttpSessionListener()); ++ ++ Collection listeners = wac.getSessionHandler().getBeans(MyHttpSessionListener.class); ++ assertNotNull(listeners); ++ ++ assertEquals(1, listeners.size()); + } + finally + { @@ -191,56 +593,65 @@ index ba83986..24ac045 100644 + } + } + - /** - * Test that listeners are called when a session expires. - * -@@ -144,7 +212,7 @@ public class SessionListenerTest - ServletHolder holder = new ServletHolder(servlet); - ServletContextHandler context = server1.addContext(contextPath); - context.addServlet(holder, servletMapping); -- TestHttpSessionListener listener = new TestHttpSessionListener(true); -+ TestHttpSessionListener listener = new TestHttpSessionListener(true.true); - context.getSessionHandler().addEventListener(listener); - - server1.start(); -@@ -171,7 +239,8 @@ public class SessionListenerTest - - assertThat(sessionId, isIn(listener.destroyedSessions)); - -- assertNull(listener.ex); -+ assertNull(listener.attributeException); -+ assertNull(listener.accessTimeException); - } - finally - { -@@ -203,7 +272,7 @@ public class SessionListenerTest - ServletHolder holder = new ServletHolder(servlet); - ServletContextHandler context = server1.addContext(contextPath); - context.addServlet(holder, servletMapping); -- TestHttpSessionListener listener = new TestHttpSessionListener(); -+ TestHttpSessionListener listener = new TestHttpSessionListener(true.true); - - context.getSessionHandler().addEventListener(listener); - -@@ -236,7 +305,8 @@ public class SessionListenerTest - - assertTrue (listener.destroyedSessions.contains("1234")); - -- assertNull(listener.ex); -+ assertNull(listener.attributeException); -+ assertNull(listener.accessTimeException); - - } - finally -@@ -245,8 +315,6 @@ public class SessionListenerTest - } - } - -- -- public static class MySessionBindingListener implements HttpSessionBindingListener, Serializable { private static final long serialVersionUID = 1L; --- -2.23.0 - + boolean unbound = false; + boolean bound = false; +- ++ + public void valueUnbound(HttpSessionBindingEvent event) + { + unbound = true; +@@ -263,38 +430,34 @@ public class SessionListenerTest + bound = true; + } + } +- +- +- ++ + public static class TestServlet extends HttpServlet + { + private static final long serialVersionUID = 1L; + public static final MySessionBindingListener bindingListener = new MySessionBindingListener(); +- + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse httpServletResponse) throws ServletException, IOException + { + String action = request.getParameter("action"); +- ++ + if ("init".equals(action)) + { + HttpSession session = request.getSession(true); + session.setAttribute("foo", bindingListener); + assertNotNull(session); +- + } + else if ("test".equals(action)) + { + HttpSession session = request.getSession(false); + assertNotNull(session); +- ++ + //invalidate existing session + session.invalidate(); + } + } + } +- ++ + public static class SimpleTestServlet extends HttpServlet + { + private static final long serialVersionUID = 1L; +@@ -306,7 +469,7 @@ public class SessionListenerTest + if ("test".equals(action)) + { + HttpSession session = request.getSession(true); +- assertTrue(session != null); ++ assertNotNull(session); + } + } + } diff --git a/jetty-9.4.15.v20190215.tar.gz b/jetty-9.4.16.v20190411.tar.gz similarity index 69% rename from jetty-9.4.15.v20190215.tar.gz rename to jetty-9.4.16.v20190411.tar.gz index 995fce98136669269e234a39806185736e642b66..0e76c17d4f0a6952b3fa1360e1483f3f8fee9925 100644 GIT binary patch delta 15878382 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

p(pNE@JZK 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