From eaf22630538d18653c3f4409c9d21936f245a07f Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Thu, 15 Aug 2019 17:58:53 -0500 Subject: [PATCH 01/55] Issue #3985 - Testcase for CookieCutter parsing issue of bad cookie Signed-off-by: Joakim Erdfelt --- .../org/eclipse/jetty/server/CookieCutterTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutterTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutterTest.java index 957d7be4916..04019ade121 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutterTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutterTest.java @@ -245,4 +245,18 @@ public class CookieCutterTest assertThat("Cookies.length", cookies.length, is(0)); } + + @Test + public void testMultipleCookies() + { + String rawCookie = "testcookie; server.id=abcd; server.detail=cfg"; + + // The first cookie "testcookie" should be ignored, per RFC6265, as it's missing the "=" sign. + + Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie); + + assertThat("Cookies.length", cookies.length, is(2)); + assertCookie("Cookies[0]", cookies[0], "server.id", "abcd", 0, null); + assertCookie("Cookies[1]", cookies[1], "server.detail", "cfg", 0, null); + } } From fba010d33d2302ec2b7356da16a6bb71a1c11fdf Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Fri, 16 Aug 2019 13:11:40 -0500 Subject: [PATCH 02/55] Issue #3985 - Updates to CookieCutter to reject no-equal cookies * If a cookie has no value it is rejected and not stored. - `name` is rejected - `name=` is accepted, with empty value Signed-off-by: Joakim Erdfelt --- .../eclipse/jetty/server/CookieCutter.java | 49 ++++++++++++++++--- .../server/CookieCutter_LenientTest.java | 19 ++++--- 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java b/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java index 8248795b14d..148e55ebe9c 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java @@ -144,14 +144,13 @@ public class CookieCutter boolean inQuoted = false; boolean quoted = false; boolean escaped = false; + boolean reject = false; int tokenstart = -1; int tokenend = -1; for (int i = 0, length = hdr.length(); i <= length; i++) { char c = i == length ? 0 : hdr.charAt(i); - // System.err.printf("i=%d/%d c=%s v=%b q=%b/%b e=%b u=%s s=%d e=%d \t%s=%s%n" ,i,length,c==0?"|":(""+c),invalue,inQuoted,quoted,escaped,unquoted,tokenstart,tokenend,name,value); - // Handle quoted values for name or value if (inQuoted) { @@ -199,7 +198,7 @@ public class CookieCutter // Handle name and value state machines if (invalue) { - // parse the value + // parse the cookie-value switch (c) { case ' ': @@ -273,7 +272,10 @@ public class CookieCutter cookie = new Cookie(name, value); if (version > 0) cookie.setVersion(version); - cookies.add(cookie); + if (!reject) + { + cookies.add(cookie); + } } } catch (Exception e) @@ -284,6 +286,7 @@ public class CookieCutter name = null; tokenstart = -1; invalue = false; + reject = false; break; } @@ -308,6 +311,19 @@ public class CookieCutter quoted = false; continue; } + + if (_compliance == CookieCompliance.RFC6265) + { + // Rejected cookie-octet characters + // US-ASCII characters excluding CTLs, + // whitespace DQUOTE, comma, semicolon, + // and backslash + if (Character.isISOControl(c) || Character.isWhitespace(c) || c == '\\') + { + reject = true; + } + } + if (tokenstart < 0) tokenstart = i; tokenend = i; @@ -316,13 +332,20 @@ public class CookieCutter } else { - // parse the name + // parse the cookie-name switch (c) { case ' ': case '\t': continue; + case ';': + // a cookie terminated with no '=' sign. + tokenstart = -1; + invalue = false; + reject = false; + continue; + case '=': if (quoted) { @@ -346,6 +369,20 @@ public class CookieCutter quoted = false; continue; } + + if (_compliance == CookieCompliance.RFC6265) + { + // Rejected cookie-octet characters + // US-ASCII characters excluding CTLs, + // whitespace DQUOTE, comma, semicolon, + // and backslash + if (Character.isISOControl(c) || Character.isWhitespace(c) || + c == ',' || c == '\\') + { + reject = true; + } + } + if (tokenstart < 0) tokenstart = i; tokenend = i; @@ -356,7 +393,7 @@ public class CookieCutter } } - _cookies = cookies.toArray(new Cookie[cookies.size()]); + _cookies = cookies.toArray(new Cookie[0]); _lastCookies = _cookies; } } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutter_LenientTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutter_LenientTest.java index 9dbafbe8ad6..6e3996679a8 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutter_LenientTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutter_LenientTest.java @@ -60,12 +60,17 @@ public class CookieCutter_LenientTest // quoted-string = ( <"> *(qdtext) <"> ) // qdtext = > + // rejected, as name cannot be DQUOTED + Arguments.of("\"a\"=bcd", null, null), + Arguments.of("\"a\"=\"b c d\"", null, null), + // lenient with spaces and EOF Arguments.of("abc=", "abc", ""), Arguments.of("abc = ", "abc", ""), Arguments.of("abc = ;", "abc", ""), Arguments.of("abc = ; ", "abc", ""), Arguments.of("abc = x ", "abc", "x"), + Arguments.of("abc = e f g ", "abc", "e f g"), Arguments.of("abc=\"\"", "abc", ""), Arguments.of("abc= \"\" ", "abc", ""), Arguments.of("abc= \"x\" ", "abc", "x"), @@ -110,17 +115,17 @@ public class CookieCutter_LenientTest // Unterminated Quotes with trailing escape Arguments.of("x=\"abc\\", "x", "\"abc\\"), - // UTF-8 values - Arguments.of("2sides=\u262F", "2sides", "\u262f"), // 2 byte - Arguments.of("currency=\"\u20AC\"", "currency", "\u20AC"), // 3 byte - Arguments.of("gothic=\"\uD800\uDF48\"", "gothic", "\uD800\uDF48"), // 4 byte + // UTF-8 raw values (not encoded) - VIOLATION of RFC6265 + Arguments.of("2sides=\u262F", "2sides", "\u262f"), // 2 byte (YIN YANG) + Arguments.of("currency=\"\u20AC\"", "currency", "\u20AC"), // 3 byte (EURO SIGN) + Arguments.of("gothic=\"\uD800\uDF48\"", "gothic", "\uD800\uDF48"), // 4 byte (GOTHIC LETTER HWAIR) // Spaces Arguments.of("foo=bar baz", "foo", "bar baz"), Arguments.of("foo=\"bar baz\"", "foo", "bar baz"), Arguments.of("z=a b c d e f g", "z", "a b c d e f g"), - // Bad tspecials usage + // Bad tspecials usage - VIOLATION of RFC6265 Arguments.of("foo=bar;baz", "foo", "bar"), Arguments.of("foo=\"bar;baz\"", "foo", "bar;baz"), Arguments.of("z=a;b,c:d;e/f[g]", "z", "a"), @@ -131,7 +136,7 @@ public class CookieCutter_LenientTest Arguments.of("x=\"$Path=/\"", "x", "$Path=/"), Arguments.of("x=\"$Path=/ $Domain=.foo.com\"", "x", "$Path=/ $Domain=.foo.com"), Arguments.of("x=\" $Path=/ $Domain=.foo.com \"", "x", " $Path=/ $Domain=.foo.com "), - Arguments.of("a=\"b; $Path=/a; c=d; $PATH=/c; e=f\"; $Path=/e/", "a", "b; $Path=/a; c=d; $PATH=/c; e=f"), + Arguments.of("a=\"b; $Path=/a; c=d; $PATH=/c; e=f\"; $Path=/e/", "a", "b; $Path=/a; c=d; $PATH=/c; e=f"), // VIOLATES RFC6265 // Lots of equals signs Arguments.of("query=b=c&d=e", "query", "b=c&d=e"), @@ -142,7 +147,7 @@ public class CookieCutter_LenientTest // Google cookies (seen in wild, has `tspecials` of ':' in value) Arguments.of("GAPS=1:A1aaaAaAA1aaAAAaa1a11a:aAaaAa-aaA1-", "GAPS", "1:A1aaaAaAA1aaAAAaa1a11a:aAaaAa-aaA1-"), - // Strong abuse of cookie spec (lots of tspecials) + // Strong abuse of cookie spec (lots of tspecials) - VIOLATION of RFC6265 Arguments.of("$Version=0; rToken=F_TOKEN''!--\"=&{()}", "rToken", "F_TOKEN''!--\"=&{()}"), // Commas that were not commas From f65e59cadfe15159550566cf3e8488deda3209f4 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Fri, 16 Aug 2019 15:13:53 -0500 Subject: [PATCH 03/55] Issue #3983 - Modernizing JarResourceTest Signed-off-by: Joakim Erdfelt --- .../jetty/util/resource/JarResourceTest.java | 160 +++++++++--------- 1 file changed, 82 insertions(+), 78 deletions(-) diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/resource/JarResourceTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/resource/JarResourceTest.java index d22642d5dac..7efcd6754b6 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/resource/JarResourceTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/resource/JarResourceTest.java @@ -18,79 +18,82 @@ package org.eclipse.jetty.util.resource; -import java.io.File; -import java.io.FilenameFilter; +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; +import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import java.util.zip.ZipFile; +import org.eclipse.jetty.toolchain.test.FS; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; -import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.instanceOf; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +@ExtendWith(WorkDirExtension.class) public class JarResourceTest { - private String testResURI = MavenTestingUtils.getTestResourcesPath().toUri().toASCIIString(); + public WorkDir workDir; @Test public void testJarFile() throws Exception { - String s = "jar:" + testResURI + "TestData/test.zip!/subdir/"; + Path testZip = MavenTestingUtils.getTestResourcePathFile("TestData/test.zip"); + String s = "jar:" + testZip.toUri().toASCIIString() + "!/subdir/"; Resource r = Resource.newResource(s); Set entries = new HashSet<>(Arrays.asList(r.list())); assertThat(entries, containsInAnyOrder("alphabet", "numbers", "subsubdir/")); - File extract = File.createTempFile("extract", null); - if (extract.exists()) - extract.delete(); - extract.mkdir(); - extract.deleteOnExit(); + Path extract = workDir.getPathFile("extract"); + FS.ensureEmpty(extract); - r.copyTo(extract); + r.copyTo(extract.toFile()); - Resource e = Resource.newResource(extract.getAbsolutePath()); + Resource e = Resource.newResource(extract.toString()); entries = new HashSet<>(Arrays.asList(e.list())); assertThat(entries, containsInAnyOrder("alphabet", "numbers", "subsubdir/")); - IO.delete(extract); - - s = "jar:" + testResURI + "TestData/test.zip!/subdir/subsubdir/"; + s = "jar:" + testZip.toUri().toASCIIString() + "!/subdir/subsubdir/"; r = Resource.newResource(s); entries = new HashSet<>(Arrays.asList(r.list())); assertThat(entries, containsInAnyOrder("alphabet", "numbers")); - extract = File.createTempFile("extract", null); - if (extract.exists()) - extract.delete(); - extract.mkdir(); - extract.deleteOnExit(); + Path extract2 = workDir.getPathFile("extract2"); + FS.ensureEmpty(extract2); - r.copyTo(extract); + r.copyTo(extract2.toFile()); - e = Resource.newResource(extract.getAbsolutePath()); + e = Resource.newResource(extract2.toString()); entries = new HashSet<>(Arrays.asList(e.list())); assertThat(entries, containsInAnyOrder("alphabet", "numbers")); - IO.delete(extract); } @Test public void testJarFileGetAllResoures() throws Exception { - String s = "jar:" + testResURI + "TestData/test.zip!/subdir/"; + Path testZip = MavenTestingUtils.getTestResourcePathFile("TestData/test.zip"); + String s = "jar:" + testZip.toUri().toASCIIString() + "!/subdir/"; Resource r = Resource.newResource(s); Collection deep = r.getAllResources(); @@ -101,16 +104,17 @@ public class JarResourceTest public void testJarFileIsContainedIn() throws Exception { - String s = "jar:" + testResURI + "TestData/test.zip!/subdir/"; + Path testZip = MavenTestingUtils.getTestResourcePathFile("TestData/test.zip"); + String s = "jar:" + testZip.toUri().toASCIIString() + "!/subdir/"; Resource r = Resource.newResource(s); - Resource container = Resource.newResource(testResURI + "TestData/test.zip"); + Resource container = Resource.newResource(testZip); - assertTrue(r instanceof JarFileResource); + assertThat(r, instanceOf(JarFileResource.class)); JarFileResource jarFileResource = (JarFileResource)r; assertTrue(jarFileResource.isContainedIn(container)); - container = Resource.newResource(testResURI + "TestData"); + container = Resource.newResource(testZip.getParent()); assertFalse(jarFileResource.isContainedIn(container)); } @@ -118,9 +122,11 @@ public class JarResourceTest public void testJarFileLastModified() throws Exception { - String s = "jar:" + testResURI + "TestData/test.zip!/subdir/numbers"; + Path testZip = MavenTestingUtils.getTestResourcePathFile("TestData/test.zip"); - try (ZipFile zf = new ZipFile(MavenTestingUtils.getTestResourceFile("TestData/test.zip"))) + String s = "jar:" + testZip.toUri().toASCIIString() + "!/subdir/numbers"; + + try (ZipFile zf = new ZipFile(testZip.toFile())) { long last = zf.getEntry("subdir/numbers").getTime(); @@ -132,73 +138,71 @@ public class JarResourceTest @Test public void testJarFileCopyToDirectoryTraversal() throws Exception { - String s = "jar:" + testResURI + "TestData/extract.zip!/"; + Path testZip = MavenTestingUtils.getTestResourcePathFile("TestData/extract.zip"); + + String s = "jar:" + testZip.toUri().toASCIIString() + "!/"; Resource r = Resource.newResource(s); - assertTrue(r instanceof JarResource); + assertThat(r, instanceOf(JarResource.class)); JarResource jarResource = (JarResource)r; - File destParent = File.createTempFile("copyjar", null); - if (destParent.exists()) - destParent.delete(); - destParent.mkdir(); - destParent.deleteOnExit(); + Path destParent = workDir.getPathFile("copyjar"); + FS.ensureEmpty(destParent); - File dest = new File(destParent.getCanonicalPath() + "/extract"); - if (dest.exists()) - dest.delete(); - dest.mkdir(); - dest.deleteOnExit(); + Path dest = destParent.toRealPath().resolve("extract"); + FS.ensureEmpty(dest); - jarResource.copyTo(dest); + jarResource.copyTo(dest.toFile()); // dest contains only the valid entry; dest.getParent() contains only the dest directory - assertEquals(1, dest.listFiles().length); - assertEquals(1, dest.getParentFile().listFiles().length); + assertEquals(1, listFiles(dest).size()); + assertEquals(1, listFiles(dest.getParent()).size()); - FilenameFilter dotdotFilenameFilter = new FilenameFilter() - { - @Override - public boolean accept(File directory, String name) - { - return name.equals("dotdot.txt"); - } - }; - assertEquals(0, dest.listFiles(dotdotFilenameFilter).length); - assertEquals(0, dest.getParentFile().listFiles(dotdotFilenameFilter).length); + DirectoryStream.Filter dotdotFilenameFilter = (path) -> + path.getFileName().toString().equalsIgnoreCase("dotdot.dot"); - FilenameFilter extractfileFilenameFilter = new FilenameFilter() - { - @Override - public boolean accept(File directory, String name) - { - return name.equals("extract-filenotdir"); - } - }; - assertEquals(0, dest.listFiles(extractfileFilenameFilter).length); - assertEquals(0, dest.getParentFile().listFiles(extractfileFilenameFilter).length); + assertEquals(0, listFiles(dest, dotdotFilenameFilter).size()); + assertEquals(0, listFiles(dest.getParent(), dotdotFilenameFilter).size()); - FilenameFilter currentDirectoryFilenameFilter = new FilenameFilter() - { - @Override - public boolean accept(File directory, String name) - { - return name.equals("current.txt"); - } - }; - assertEquals(1, dest.listFiles(currentDirectoryFilenameFilter).length); - assertEquals(0, dest.getParentFile().listFiles(currentDirectoryFilenameFilter).length); + DirectoryStream.Filter extractfileFilenameFilter = (path) -> + path.getFileName().toString().equalsIgnoreCase("extract-filenotdir"); - IO.delete(dest); - assertFalse(dest.exists()); + assertEquals(0, listFiles(dest, extractfileFilenameFilter).size()); + assertEquals(0, listFiles(dest.getParent(), extractfileFilenameFilter).size()); + + DirectoryStream.Filter currentDirectoryFilenameFilter = (path) -> + path.getFileName().toString().equalsIgnoreCase("current.txt"); + + assertEquals(1, listFiles(dest, currentDirectoryFilenameFilter).size()); + assertEquals(0, listFiles(dest.getParent(), currentDirectoryFilenameFilter).size()); } @Test public void testEncodedFileName() throws Exception { - String s = "jar:" + testResURI + "TestData/test.zip!/file%20name.txt"; + Path testZip = MavenTestingUtils.getTestResourcePathFile("TestData/test.zip"); + + String s = "jar:" + testZip.toUri().toASCIIString() + "!/file%20name.txt"; Resource r = Resource.newResource(s); assertTrue(r.exists()); } + + private List listFiles(Path dir) throws IOException + { + return Files.list(dir).collect(Collectors.toList()); + } + + private List listFiles(Path dir, DirectoryStream.Filter filter) throws IOException + { + List results = new ArrayList<>(); + try (DirectoryStream filteredDirStream = Files.newDirectoryStream(dir, filter)) + { + for (Path path : filteredDirStream) + { + results.add(path); + } + return results; + } + } } From 472ede48cd35748c4d3d7a5206b96bc068757f57 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Fri, 16 Aug 2019 15:48:42 -0500 Subject: [PATCH 04/55] Issue #3983 - JarFileResource directory listing is invalid + Correcting encoded path searching + Adding more unit tests to ensure no regression Signed-off-by: Joakim Erdfelt --- .../jetty/util/resource/JarFileResource.java | 14 ++-- .../jetty/util/resource/JarResourceTest.java | 78 +++++++++++++++++- .../src/test/resources/jar-file-resource.jar | Bin 0 -> 3172 bytes 3 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 jetty-util/src/test/resources/jar-file-resource.jar diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/JarFileResource.java b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/JarFileResource.java index 044a244ecaf..3ceb0a55cd4 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/JarFileResource.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/JarFileResource.java @@ -29,7 +29,6 @@ import java.util.List; import java.util.jar.JarEntry; import java.util.jar.JarFile; -import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -116,9 +115,11 @@ public class JarFileResource extends JarResource _jarFile = null; _list = null; - int sep = _urlString.lastIndexOf("!/"); - _jarUrl = _urlString.substring(0, sep + 2); - _path = URIUtil.decodePath(_urlString.substring(sep + 2)); + // Work with encoded URL path + String urlFilePath = _url.getPath(); + int sep = urlFilePath.lastIndexOf("!/"); + _jarUrl = urlFilePath.substring(0, sep + 2); + _path = URIUtil.decodePath(urlFilePath.substring(sep + 2)); if (_path.length() == 0) _path = null; _jarFile = _jarConnection.getJarFile(); @@ -316,11 +317,12 @@ public class JarFileResource extends JarResource } Enumeration e = jarFile.entries(); - String dir = _urlString.substring(_urlString.lastIndexOf("!/") + 2); + String encodedDir = _urlString.substring(_urlString.lastIndexOf("!/") + 2); + String dir = URIUtil.decodePath(encodedDir); while (e.hasMoreElements()) { JarEntry entry = e.nextElement(); - String name = StringUtil.replace(entry.getName(), '\\', '/'); + String name = entry.getName(); if (!name.startsWith(dir) || name.length() == dir.length()) { continue; diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/resource/JarResourceTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/resource/JarResourceTest.java index 7efcd6754b6..1fc6f4afe5e 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/resource/JarResourceTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/resource/JarResourceTest.java @@ -19,6 +19,7 @@ package org.eclipse.jetty.util.resource; import java.io.IOException; +import java.net.URI; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; @@ -41,6 +42,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.instanceOf; +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.assertTrue; @@ -138,9 +140,9 @@ public class JarResourceTest @Test public void testJarFileCopyToDirectoryTraversal() throws Exception { - Path testZip = MavenTestingUtils.getTestResourcePathFile("TestData/extract.zip"); + Path extractZip = MavenTestingUtils.getTestResourcePathFile("TestData/extract.zip"); - String s = "jar:" + testZip.toUri().toASCIIString() + "!/"; + String s = "jar:" + extractZip.toUri().toASCIIString() + "!/"; Resource r = Resource.newResource(s); assertThat(r, instanceOf(JarResource.class)); @@ -188,6 +190,78 @@ public class JarResourceTest assertTrue(r.exists()); } + @Test + public void testJarFileResourceList() throws Exception + { + Path testJar = MavenTestingUtils.getTestResourcePathFile("jar-file-resource.jar"); + String uri = "jar:" + testJar.toUri().toASCIIString() + "!/"; + + Resource resource = new JarFileResource(URI.create(uri).toURL()); + Resource rez = resource.addPath("rez/"); + + assertThat("path /rez/ is a dir", rez.isDirectory(), is(true)); + + List actual = Arrays.asList(rez.list()); + String[] expected = new String[]{ + "one", + "aaa", + "bbb", + "oddities/", + "another dir/", + "ccc", + "deep/", + }; + assertThat("Dir contents", actual, containsInAnyOrder(expected)); + } + + /** + * Test getting a file listing of a Directory in a JAR + * Where the JAR entries contain names that are URI encoded / escaped + */ + @Test + public void testJarFileResourceList_PreEncodedEntries() throws Exception + { + Path testJar = MavenTestingUtils.getTestResourcePathFile("jar-file-resource.jar"); + String uri = "jar:" + testJar.toUri().toASCIIString() + "!/"; + + Resource resource = new JarFileResource(URI.create(uri).toURL()); + Resource rez = resource.addPath("rez/oddities/"); + + assertThat("path /rez/oddities/ is a dir", rez.isDirectory(), is(true)); + + List actual = Arrays.asList(rez.list()); + String[] expected = new String[]{ + ";", + "#hashcode", + "index.html#fragment", + "other%2fkind%2Fof%2fslash", // pre-encoded / escaped + "a file with a space", + ";\" onmousedown=\"alert(document.location)\"", + "some\\slash\\you\\got\\there" // not encoded, stored as backslash native + }; + assertThat("Dir contents", actual, containsInAnyOrder(expected)); + } + + @Test + public void testJarFileResourceList_DirWithSpace() throws Exception + { + Path testJar = MavenTestingUtils.getTestResourcePathFile("jar-file-resource.jar"); + String uri = "jar:" + testJar.toUri().toASCIIString() + "!/"; + + Resource resource = new JarFileResource(URI.create(uri).toURL()); + Resource anotherDir = resource.addPath("rez/another dir/"); + + assertThat("path /rez/another dir/ is a dir", anotherDir.isDirectory(), is(true)); + + List actual = Arrays.asList(anotherDir.list()); + String[] expected = new String[]{ + "a file.txt", + "another file.txt", + "..\\a different file.txt", + }; + assertThat("Dir contents", actual, containsInAnyOrder(expected)); + } + private List listFiles(Path dir) throws IOException { return Files.list(dir).collect(Collectors.toList()); diff --git a/jetty-util/src/test/resources/jar-file-resource.jar b/jetty-util/src/test/resources/jar-file-resource.jar new file mode 100644 index 0000000000000000000000000000000000000000..de93283a8373dd95b7b35282f208069a47447aef GIT binary patch literal 3172 zcmb7FQD|FL7{2WiSL4*KX0nOswOwq=(iE!Ghw9TdO}#cvUD`b?3b)D4k}bKnBq?hG zzD%Pm2tt_;g+9pG!%_l6=Zm-rlLT(eZGxrF4LXo6Q&0wqC{D`o|L?u+Irl6n`j-PY z<^11w{{Q^v`vSran}g%JIL?2vdz_=f#@V=(lpDJLEyo35my_%2>f(Mm^s%4X#O;J1 zxJ!|xnrF&gRc*VAj(geSUm-}^*>zwkd9xd~?1n`^ZnXgsMe#sY^-OW+%jLsf9pJO+ zYnOIGGA;seyA8nf^t1_Z;lh3cpu#_Ujs`G_oly#wG9CqZy^KgCo{3B8A;R^0(z^9A zcI$``RBH`k(C_#7#wOhU<>fc)jcVia&z*V&omYRWJOw#>EfqW`TT!26QTt+IIu=$U zk`VOOw9!d-!&43Tf~wC`_tnQoCspmJ>KT|)1#Rq(LA3Vi>(^ZX(`O+XV2F5@C@x2& zId3eJO!P%l;)$drXG}`pvQX88V+^y&_D%PmuMq1QSfp8OU!P~$luS%YxrU>sfYvqq zni7TYbOK7P4OM-EZ>e=nm{7-$y6dB!`j19k*}}EAu{}-;(+YDfJj*2V(Rf1QPscMc zUgXm=Vi;QKu4{wp=%lY^>$$Vw+UKa)&sK{S$3oWO+E-jS;>e!;vS>`M_+Ip`8!&N^ z`Z%VWFR`p6y}TkPmD#iuQBKRR_KFE9m2pRu@GLggn^3}HCa%Z>z2=2(Ln-&au8&Uf<;$9(X`AeMTto zeIu19>zB(%;wkF+3^Tj&DX_bZtMX>%%*iuHta7{Y&cm=BHHK9BG6TrB0yK|;6QILt z#(3dNGQn>YZq^=$RDAjE!TVc)d%YnMii}2~dvZpfvj4^LQycLLhr{M&G@B9QuCG6x zd+|Z>`ZZwH4(t)OG$KheROkO-co1l7+uYooNmg2c@j9#zSpG_RZPl4C6dVTqjgjMT z;QO||@q63h=5jfKYWDXNU);6heEq;;v@js#CLQW$UA*Xlx$jVt(gN48KvulAmDT+ e$}z32*31v-Oc3XQV2A&3+#dML!Fg8oU;hHK@Z9nM literal 0 HcmV?d00001 From 1254f6eb671a420e470133062b657ebab4fd0ddb Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Fri, 16 Aug 2019 15:57:26 -0500 Subject: [PATCH 05/55] Issue #3985 - Fixing RequestTest + Moving overly bad cookie to CookieCutter_LenientTest + Changing expectation in RequestTest.testCookie() Signed-off-by: Joakim Erdfelt --- .../org/eclipse/jetty/server/CookieCutter_LenientTest.java | 1 + .../src/test/java/org/eclipse/jetty/server/RequestTest.java | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutter_LenientTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutter_LenientTest.java index 6e3996679a8..3f4ba333d96 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutter_LenientTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutter_LenientTest.java @@ -130,6 +130,7 @@ public class CookieCutter_LenientTest Arguments.of("foo=\"bar;baz\"", "foo", "bar;baz"), Arguments.of("z=a;b,c:d;e/f[g]", "z", "a"), Arguments.of("z=\"a;b,c:d;e/f[g]\"", "z", "a;b,c:d;e/f[g]"), + Arguments.of("name=quoted=\"\\\"badly\\\"\"", null, null), // someone attempting to escape a DQUOTE from within a DQUOTED pair) // Quoted with other Cookie keywords Arguments.of("x=\"$Version=0\"", "x", "$Version=0"), diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java index 69a29dc6b71..5089ce9593b 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java @@ -1515,9 +1515,7 @@ public class RequestTest "\n" ); assertTrue(response.startsWith("HTTP/1.1 200 OK")); - assertEquals(1, cookies.size()); - assertEquals("name", cookies.get(0).getName()); - assertEquals("quoted=\"\\\"badly\\\"\"", cookies.get(0).getValue()); + assertEquals(0, cookies.size()); // this is an invalid cookie cookies.clear(); response = _connector.getResponse( From 407b5643200dc480877e2585845478c563038639 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Mon, 19 Aug 2019 12:52:43 -0500 Subject: [PATCH 06/55] Issue #4000 - adding unit test for raw Resource access for unicode file. Signed-off-by: Joakim Erdfelt --- .../util/resource/FileSystemResourceTest.java | 98 +++++++++++++------ 1 file changed, 70 insertions(+), 28 deletions(-) diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/resource/FileSystemResourceTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/resource/FileSystemResourceTest.java index 9fa220d6402..bc22532475f 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/resource/FileSystemResourceTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/resource/FileSystemResourceTest.java @@ -18,7 +18,22 @@ package org.eclipse.jetty.util.resource; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThanOrEqualTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.junit.jupiter.api.condition.OS.LINUX; +import static org.junit.jupiter.api.condition.OS.MAC; +import static org.junit.jupiter.api.condition.OS.WINDOWS; + import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -58,20 +73,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.lessThanOrEqualTo; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assumptions.assumeTrue; -import static org.junit.jupiter.api.condition.OS.LINUX; -import static org.junit.jupiter.api.condition.OS.MAC; -import static org.junit.jupiter.api.condition.OS.WINDOWS; - @ExtendWith(WorkDirExtension.class) public class FileSystemResourceTest { @@ -318,7 +319,39 @@ public class FileSystemResourceTest } } } - + + @ParameterizedTest + @MethodSource("fsResourceProvider") + public void testAccessUniCodeFile(Class resourceClass) throws Exception + { + Path dir = workDir.getEmptyPathDir(); + + String readableRootDir = findRootDir(dir.getFileSystem()); + assumeTrue(readableRootDir != null, "Readable Root Dir found"); + + Path subdir = dir.resolve("sub"); + Files.createDirectories(subdir); + + touchFile(subdir.resolve("swedish-å.txt"), "hi a-with-circle"); + touchFile(subdir.resolve("swedish-ä.txt"), "hi a-with-two-dots"); + touchFile(subdir.resolve("swedish-ö.txt"), "hi o-with-two-dots"); + + try (Resource base = newResource(resourceClass, subdir.toFile())) + { + Resource refA1 = base.addPath("swedish-å.txt"); + Resource refA2 = base.addPath("swedish-ä.txt"); + Resource refO1 = base.addPath("swedish-ö.txt"); + + assertThat("Ref A1", refA1.exists(), is(true)); + assertThat("Ref A2", refA2.exists(), is(true)); + assertThat("Ref O1", refO1.exists(), is(true)); + + assertThat("Ref A1 contents", toString(refA1), is("hi a-with-circle")); + assertThat("Ref A2 contents", toString(refA2), is("hi a-with-two-dots")); + assertThat("Ref O1 contents", toString(refO1), is("hi o-with-two-dots")); + } + } + private String findRootDir(FileSystem fs) throws IOException { // look for a directory off of a root path @@ -418,12 +451,7 @@ public class FileSystemResourceTest Files.createDirectories(dir); Path file = dir.resolve("foo"); - - try (StringReader reader = new StringReader("foo"); - BufferedWriter writer = Files.newBufferedWriter(file)) - { - IO.copy(reader, writer); - } + touchFile(file, "foo"); long expected = Files.size(file); @@ -433,7 +461,7 @@ public class FileSystemResourceTest assertThat("foo.length", res.length(), is(expected)); } } - + @ParameterizedTest @MethodSource("fsResourceProvider") public void testLength_NotExists(Class resourceClass) throws Exception @@ -512,12 +540,7 @@ public class FileSystemResourceTest Path file = dir.resolve("foo"); String content = "Foo is here"; - - try (StringReader reader = new StringReader(content); - BufferedWriter writer = Files.newBufferedWriter(file)) - { - IO.copy(reader, writer); - } + touchFile(file, content); try (Resource base = newResource(resourceClass, dir.toFile())) { @@ -1506,4 +1529,23 @@ public class FileSystemResourceTest assertThat("getAlias()", resource.getAlias(), nullValue()); } } + + private String toString(Resource resource) throws IOException + { + try (InputStream inputStream = resource.getInputStream(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) + { + IO.copy(inputStream, outputStream); + return outputStream.toString("utf-8"); + } + } + + private void touchFile(Path outputFile, String content) throws IOException + { + try (StringReader reader = new StringReader(content); + BufferedWriter writer = Files.newBufferedWriter(outputFile)) + { + IO.copy(reader, writer); + } + } } From ef3f696a1167b631999de6b9fa7bae472437bde0 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Mon, 19 Aug 2019 13:52:22 -0500 Subject: [PATCH 07/55] Issue #4000 - PathResource alias detection work around alt UTF-8 style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit + OSX File is `swedish-å.txt` + OSX has NFD form UTF-8 characters. `swedish-a%CC%8A.txt` + HTTP uses normal form UTF-8. `swedish-%C3%A5.txt` + A HTTP GET request should work against the resource being requested, regardless of UTF-8 style used. Signed-off-by: Joakim Erdfelt --- .../jetty/servlet/DefaultServletTest.java | 60 ++++++++++++++----- .../jetty/util/resource/PathResource.java | 48 ++++++++++----- .../util/resource/FileSystemResourceTest.java | 43 +++++++++---- 3 files changed, 111 insertions(+), 40 deletions(-) diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/DefaultServletTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/DefaultServletTest.java index 5b41d82917e..73038f1fbe4 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/DefaultServletTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/DefaultServletTest.java @@ -18,6 +18,20 @@ package org.eclipse.jetty.servlet; +import static org.eclipse.jetty.http.HttpFieldsMatchers.containsHeader; +import static org.eclipse.jetty.http.HttpFieldsMatchers.containsHeaderValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + import java.io.File; import java.io.IOException; import java.io.OutputStream; @@ -35,6 +49,7 @@ import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; + import javax.servlet.DispatcherType; import javax.servlet.Filter; import javax.servlet.FilterChain; @@ -73,20 +88,6 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; -import static org.eclipse.jetty.http.HttpFieldsMatchers.containsHeader; -import static org.eclipse.jetty.http.HttpFieldsMatchers.containsHeaderValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.anyOf; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.endsWith; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -import static org.hamcrest.Matchers.startsWith; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assumptions.assumeTrue; - @ExtendWith(WorkDirExtension.class) public class DefaultServletTest { @@ -2009,6 +2010,37 @@ public class DefaultServletTest response = HttpTester.parseResponse(rawResponse); assertThat(response.toString(), response.getStatus(), is(HttpStatus.PRECONDITION_FAILED_412)); } + + @Test + public void testGetUnicodeFile() throws Exception + { + FS.ensureEmpty(docRoot); + System.err.printf("docRoot is %s%n", docRoot); + + context.addServlet(DefaultServlet.class, "/"); + + createFile(docRoot.resolve("swedish-å.txt"), "hi a-with-circle"); + createFile(docRoot.resolve("swedish-ä.txt"), "hi a-with-two-dots"); + createFile(docRoot.resolve("swedish-ö.txt"), "hi o-with-two-dots"); + + String rawResponse; + HttpTester.Response response; + + rawResponse = connector.getResponse("GET /context/swedish-%C3%A5.txt HTTP/1.1\r\nHost:test\r\nConnection:close\r\n\r\n"); + response = HttpTester.parseResponse(rawResponse); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + assertThat(response.getContent(), is("hi a-with-circle")); + + rawResponse = connector.getResponse("GET /context/swedish-%C3%A4.txt HTTP/1.1\r\nHost:test\r\nConnection:close\r\n\r\n"); + response = HttpTester.parseResponse(rawResponse); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + assertThat(response.getContent(), is("hi a-with-two-dots")); + + rawResponse = connector.getResponse("GET /context/swedish-%C3%B6.txt HTTP/1.1\r\nHost:test\r\nConnection:close\r\n\r\n"); + response = HttpTester.parseResponse(rawResponse); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + assertThat(response.getContent(), is("hi o-with-two-dots")); + } public static class OutputFilter implements Filter { diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java index 92b866876fc..03d39720a91 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java @@ -80,7 +80,15 @@ public class PathResource extends Resource { try { - return Paths.get(uri).toRealPath(FOLLOW_LINKS); + Path followed = Paths.get(uri).toRealPath(FOLLOW_LINKS); + if (!isSame(path, followed)) + { + return followed; + } + else + { + return null; + } } catch (IOException ignored) { @@ -137,23 +145,11 @@ public class PathResource extends Resource * We also cannot rely on a.compareTo(b) as this is roughly equivalent * in implementation to a.equals(b) */ - - int absCount = abs.getNameCount(); - int realCount = real.getNameCount(); - if (absCount != realCount) + + if (!isSame(abs, real)) { - // different number of segments return real; } - - // compare each segment of path, backwards - for (int i = realCount - 1; i >= 0; i--) - { - if (!abs.getName(i).toString().equals(real.getName(i).toString())) - { - return real; - } - } } } catch (IOException e) @@ -166,6 +162,28 @@ public class PathResource extends Resource } return null; } + + private boolean isSame(Path pathA, Path pathB) + { + int aCount = pathA.getNameCount(); + int bCount = pathB.getNameCount(); + if (aCount != bCount) + { + // different number of segments + return false; + } + + // compare each segment of path, backwards + for (int i = bCount - 1; i >= 0; i--) + { + if (!pathA.getName(i).toString().equals(pathB.getName(i).toString())) + { + return false; + } + } + + return true; + } /** * Construct a new PathResource from a File object. diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/resource/FileSystemResourceTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/resource/FileSystemResourceTest.java index bc22532475f..620f573cfd0 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/resource/FileSystemResourceTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/resource/FileSystemResourceTest.java @@ -342,9 +342,13 @@ public class FileSystemResourceTest Resource refA2 = base.addPath("swedish-ä.txt"); Resource refO1 = base.addPath("swedish-ö.txt"); - assertThat("Ref A1", refA1.exists(), is(true)); - assertThat("Ref A2", refA2.exists(), is(true)); - assertThat("Ref O1", refO1.exists(), is(true)); + assertThat("Ref A1 exists", refA1.exists(), is(true)); + assertThat("Ref A2 exists", refA2.exists(), is(true)); + assertThat("Ref O1 exists", refO1.exists(), is(true)); + + assertThat("Ref A1 alias", refA1.isAlias(), is(false)); + assertThat("Ref A2 alias", refA2.isAlias(), is(false)); + assertThat("Ref O1 alias", refO1.isAlias(), is(false)); assertThat("Ref A1 contents", toString(refA1), is("hi a-with-circle")); assertThat("Ref A2 contents", toString(refA2), is("hi a-with-two-dots")); @@ -1416,11 +1420,19 @@ public class FileSystemResourceTest Resource r = base.addPath("//foo.txt"); assertThat("getURI()", r.getURI().toASCIIString(), containsString("//foo.txt")); - - assertThat("isAlias()", r.isAlias(), is(true)); - assertThat("getAlias()", r.getAlias(), notNullValue()); - assertThat("getAlias()", r.getAlias().toASCIIString(), containsString("/foo.txt")); assertThat("Exists: " + r, r.exists(), is(true)); + + if (PathResource.class.isAssignableFrom(resourceClass)) + { + assertThat("isAlias()", r.isAlias(), is(false)); + assertThat("getAlias()", r.getAlias(), nullValue()); + } + else + { + assertThat("isAlias()", r.isAlias(), is(true)); + assertThat("getAlias()", r.getAlias(), notNullValue()); + assertThat("getAlias()", r.getAlias().toASCIIString(), containsString("/foo.txt")); + } } catch (InvalidPathException e) { @@ -1449,10 +1461,19 @@ public class FileSystemResourceTest Resource r = base.addPath("aa//foo.txt"); assertThat("getURI()", r.getURI().toASCIIString(), containsString("aa//foo.txt")); - - assertThat("isAlias()", r.isAlias(), is(true)); - assertThat("getAlias()", r.getAlias(), notNullValue()); - assertThat("getAlias()", r.getAlias().toASCIIString(), containsString("aa/foo.txt")); + + if (PathResource.class.isAssignableFrom(resourceClass)) + { + assertThat("isAlias()", r.isAlias(), is(false)); + assertThat("getAlias()", r.getAlias(), nullValue()); + } + else + { + assertThat("isAlias()", r.isAlias(), is(true)); + assertThat("getAlias()", r.getAlias(), notNullValue()); + assertThat("getAlias()", r.getAlias().toASCIIString(), containsString("aa/foo.txt")); + } + assertThat("Exists: " + r, r.exists(), is(true)); } catch (InvalidPathException e) From 30bd3914cfbbee451a1b0f9547993ed8a0489be7 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Wed, 21 Aug 2019 12:21:04 -0500 Subject: [PATCH 08/55] Issue #4000 - SameFileAliasChecker to help with NFD/NFC + Adding SameFileAliasChecker to help with environments where the Alias and the Path point to the same file, by relying on the FileSystem and Path implementation to make the determination if the two Path references truly point to the same file. + Minor cleanup of FileSystemResourceTest + Additional DefaultServletTest for this UTF-8 differences Signed-off-by: Joakim Erdfelt --- .../jetty/server/SameFileAliasChecker.java | 78 +++++++++++++ .../handler/AllowSymLinkAliasChecker.java | 2 +- .../jetty/servlet/DefaultServletTest.java | 103 +++++++++++------ .../jetty/util/resource/PathResource.java | 104 ++++++++++-------- .../util/resource/FileSystemResourceTest.java | 63 ++++------- 5 files changed, 232 insertions(+), 118 deletions(-) create mode 100644 jetty-server/src/main/java/org/eclipse/jetty/server/SameFileAliasChecker.java diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/SameFileAliasChecker.java b/jetty-server/src/main/java/org/eclipse/jetty/server/SameFileAliasChecker.java new file mode 100644 index 00000000000..805620745c4 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/SameFileAliasChecker.java @@ -0,0 +1,78 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.server; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.eclipse.jetty.server.handler.ContextHandler.AliasCheck; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.resource.PathResource; +import org.eclipse.jetty.util.resource.Resource; + +/** + * Alias checking for working with FileSystems that normalize access to the + * File System. + *

+ * The Java {@link Files#isSameFile(Path, Path)} method is used to determine + * if the requested file is the same as the alias file. + *

+ *

+ * For File Systems that are case insensitive (eg: Microsoft Windows FAT32 and NTFS), + * the access to the file can be in any combination or style of upper and lowercase. + *

+ *

+ * For File Systems that normalize UTF-8 access (eg: Mac OSX on HFS+ or APFS, + * or Linux on XFS) the the actual file could be stored using UTF-16, + * but be accessed using NFD UTF-8 or NFC UTF-8 for the same file. + *

+ */ +public class SameFileAliasChecker implements AliasCheck +{ + private static final Logger LOG = Log.getLogger(SameFileAliasChecker.class); + + @Override + public boolean check(String uri, Resource resource) + { + // Only support PathResource alias checking + if (!(resource instanceof PathResource)) + return false; + + try + { + PathResource pathResource = (PathResource)resource; + Path path = pathResource.getPath(); + Path alias = pathResource.getAliasPath(); + + if (Files.isSameFile(path, alias)) + { + if (LOG.isDebugEnabled()) + LOG.debug("Allow alias to same file {} --> {}", path, alias); + return true; + } + } + catch (IOException e) + { + LOG.ignore(e); + } + return false; + } +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AllowSymLinkAliasChecker.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AllowSymLinkAliasChecker.java index c6186885d9a..6bebff65c4b 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AllowSymLinkAliasChecker.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AllowSymLinkAliasChecker.java @@ -52,7 +52,7 @@ public class AllowSymLinkAliasChecker implements AliasCheck Path path = pathResource.getPath(); Path alias = pathResource.getAliasPath(); - if (path.equals(alias)) + if (PathResource.isSame(alias, path)) return false; // Unknown why this is an alias if (hasSymbolicLink(path) && Files.isSameFile(path, alias)) diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/DefaultServletTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/DefaultServletTest.java index 73038f1fbe4..70159304887 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/DefaultServletTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/DefaultServletTest.java @@ -18,20 +18,6 @@ package org.eclipse.jetty.servlet; -import static org.eclipse.jetty.http.HttpFieldsMatchers.containsHeader; -import static org.eclipse.jetty.http.HttpFieldsMatchers.containsHeaderValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.anyOf; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.endsWith; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -import static org.hamcrest.Matchers.startsWith; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assumptions.assumeTrue; - import java.io.File; import java.io.IOException; import java.io.OutputStream; @@ -49,7 +35,6 @@ import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; - import javax.servlet.DispatcherType; import javax.servlet.Filter; import javax.servlet.FilterChain; @@ -69,12 +54,14 @@ import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.ResourceContentFactory; import org.eclipse.jetty.server.ResourceService; +import org.eclipse.jetty.server.SameFileAliasChecker; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.AllowSymLinkAliasChecker; import org.eclipse.jetty.toolchain.test.FS; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; +import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.log.StacklessLogging; import org.eclipse.jetty.util.resource.PathResource; import org.eclipse.jetty.util.resource.Resource; @@ -88,6 +75,21 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.eclipse.jetty.http.HttpFieldsMatchers.containsHeader; +import static org.eclipse.jetty.http.HttpFieldsMatchers.containsHeaderValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + @ExtendWith(WorkDirExtension.class) public class DefaultServletTest { @@ -2012,34 +2014,73 @@ public class DefaultServletTest } @Test - public void testGetUnicodeFile() throws Exception + public void testGetUtf8NfcFile() throws Exception { FS.ensureEmpty(docRoot); - System.err.printf("docRoot is %s%n", docRoot); - + context.addServlet(DefaultServlet.class, "/"); - - createFile(docRoot.resolve("swedish-å.txt"), "hi a-with-circle"); - createFile(docRoot.resolve("swedish-ä.txt"), "hi a-with-two-dots"); - createFile(docRoot.resolve("swedish-ö.txt"), "hi o-with-two-dots"); - + context.addAliasCheck(new SameFileAliasChecker()); + + // UTF-8 NFC format + String filename = "swedish-" + new String(TypeUtil.fromHexString("C3A5"), UTF_8) + ".txt"; + createFile(docRoot.resolve(filename), "hi a-with-circle"); + String rawResponse; HttpTester.Response response; - + + // Request as UTF-8 NFC rawResponse = connector.getResponse("GET /context/swedish-%C3%A5.txt HTTP/1.1\r\nHost:test\r\nConnection:close\r\n\r\n"); response = HttpTester.parseResponse(rawResponse); assertThat(response.getStatus(), is(HttpStatus.OK_200)); assertThat(response.getContent(), is("hi a-with-circle")); - - rawResponse = connector.getResponse("GET /context/swedish-%C3%A4.txt HTTP/1.1\r\nHost:test\r\nConnection:close\r\n\r\n"); + + // Request as UTF-8 NFD + rawResponse = connector.getResponse("GET /context/swedish-a%CC%8A.txt HTTP/1.1\r\nHost:test\r\nConnection:close\r\n\r\n"); + response = HttpTester.parseResponse(rawResponse); + if (OS.MAC.isCurrentOs()) + { + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + assertThat(response.getContent(), is("hi a-with-circle")); + } + else + { + assertThat(response.getStatus(), is(HttpStatus.NOT_FOUND_404)); + } + } + + @Test + public void testGetUtf8NfdFile() throws Exception + { + FS.ensureEmpty(docRoot); + + context.addServlet(DefaultServlet.class, "/"); + context.addAliasCheck(new SameFileAliasChecker()); + + // UTF-8 NFD format + String filename = "swedish-" + new String(TypeUtil.fromHexString("61CC8A"), UTF_8) + ".txt"; + createFile(docRoot.resolve(filename), "hi a-with-circle"); + + String rawResponse; + HttpTester.Response response; + + // Request as UTF-8 NFD + rawResponse = connector.getResponse("GET /context/swedish-a%CC%8A.txt HTTP/1.1\r\nHost:test\r\nConnection:close\r\n\r\n"); response = HttpTester.parseResponse(rawResponse); assertThat(response.getStatus(), is(HttpStatus.OK_200)); - assertThat(response.getContent(), is("hi a-with-two-dots")); - - rawResponse = connector.getResponse("GET /context/swedish-%C3%B6.txt HTTP/1.1\r\nHost:test\r\nConnection:close\r\n\r\n"); + assertThat(response.getContent(), is("hi a-with-circle")); + + // Request as UTF-8 NFC + rawResponse = connector.getResponse("GET /context/swedish-%C3%A5.txt HTTP/1.1\r\nHost:test\r\nConnection:close\r\n\r\n"); response = HttpTester.parseResponse(rawResponse); - assertThat(response.getStatus(), is(HttpStatus.OK_200)); - assertThat(response.getContent(), is("hi o-with-two-dots")); + if (OS.MAC.isCurrentOs()) + { + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + assertThat(response.getContent(), is("hi a-with-circle")); + } + else + { + assertThat(response.getStatus(), is(HttpStatus.NOT_FOUND_404)); + } } public static class OutputFilter implements Filter diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java index 03d39720a91..c6ba1d3fe04 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java @@ -80,15 +80,7 @@ public class PathResource extends Resource { try { - Path followed = Paths.get(uri).toRealPath(FOLLOW_LINKS); - if (!isSame(path, followed)) - { - return followed; - } - else - { - return null; - } + return Paths.get(uri).toRealPath(FOLLOW_LINKS); } catch (IOException ignored) { @@ -112,40 +104,6 @@ public class PathResource extends Resource { Path real = abs.toRealPath(FOLLOW_LINKS); - /* - * If the real path is not the same as the absolute path - * then we know that the real path is the alias for the - * provided path. - * - * For OS's that are case insensitive, this should - * return the real (on-disk / case correct) version - * of the path. - * - * We have to be careful on Windows and OSX. - * - * Assume we have the following scenario - * Path a = new File("foo").toPath(); - * Files.createFile(a); - * Path b = new File("FOO").toPath(); - * - * There now exists a file called "foo" on disk. - * Using Windows or OSX, with a Path reference of - * "FOO", "Foo", "fOO", etc.. means the following - * - * | OSX | Windows | Linux - * -----------------------+---------+------------+--------- - * Files.exists(a) | True | True | True - * Files.exists(b) | True | True | False - * Files.isSameFile(a,b) | True | True | False - * a.equals(b) | False | True | False - * - * See the javadoc for Path.equals() for details about this FileSystem - * behavior difference - * - * We also cannot rely on a.compareTo(b) as this is roughly equivalent - * in implementation to a.equals(b) - */ - if (!isSame(abs, real)) { return real; @@ -162,8 +120,62 @@ public class PathResource extends Resource } return null; } - - private boolean isSame(Path pathA, Path pathB) + + /** + * Test if the paths are the same. + * + *

+ * If the real path is not the same as the absolute path + * then we know that the real path is the alias for the + * provided path. + *

+ * + *

+ * For OS's that are case insensitive, this should + * return the real (on-disk / case correct) version + * of the path. + *

+ * + *

+ * We have to be careful on Windows and OSX. + *

+ * + *

+ * Assume we have the following scenario: + *

+ * + *
+     *   Path a = new File("foo").toPath();
+     *   Files.createFile(a);
+     *   Path b = new File("FOO").toPath();
+     * 
+ * + *

+ * There now exists a file called {@code foo} on disk. + * Using Windows or OSX, with a Path reference of + * {@code FOO}, {@code Foo}, {@code fOO}, etc.. means the following + *

+ * + *
+     *                        |  OSX    |  Windows   |  Linux
+     * -----------------------+---------+------------+---------
+     * Files.exists(a)        |  True   |  True      |  True
+     * Files.exists(b)        |  True   |  True      |  False
+     * Files.isSameFile(a,b)  |  True   |  True      |  False
+     * a.equals(b)            |  False  |  True      |  False
+     * 
+ * + *

+ * See the javadoc for Path.equals() for details about this FileSystem + * behavior difference + *

+ * + *

+ * We also cannot rely on a.compareTo(b) as this is roughly equivalent + * in implementation to a.equals(b) + *

+ */ + public static boolean isSame(Path pathA, Path pathB) { int aCount = pathA.getNameCount(); int bCount = pathB.getNameCount(); @@ -174,7 +186,7 @@ public class PathResource extends Resource } // compare each segment of path, backwards - for (int i = bCount - 1; i >= 0; i--) + for (int i = bCount; i-- > 0; ) { if (!pathA.getName(i).toString().equals(pathB.getName(i).toString())) { diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/resource/FileSystemResourceTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/resource/FileSystemResourceTest.java index 620f573cfd0..49d1ffd2d50 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/resource/FileSystemResourceTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/resource/FileSystemResourceTest.java @@ -18,20 +18,6 @@ package org.eclipse.jetty.util.resource; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.lessThanOrEqualTo; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assumptions.assumeTrue; -import static org.junit.jupiter.api.condition.OS.LINUX; -import static org.junit.jupiter.api.condition.OS.MAC; -import static org.junit.jupiter.api.condition.OS.WINDOWS; - import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; import java.io.File; @@ -73,6 +59,20 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThanOrEqualTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.junit.jupiter.api.condition.OS.LINUX; +import static org.junit.jupiter.api.condition.OS.MAC; +import static org.junit.jupiter.api.condition.OS.WINDOWS; + @ExtendWith(WorkDirExtension.class) public class FileSystemResourceTest { @@ -465,7 +465,7 @@ public class FileSystemResourceTest assertThat("foo.length", res.length(), is(expected)); } } - + @ParameterizedTest @MethodSource("fsResourceProvider") public void testLength_NotExists(Class resourceClass) throws Exception @@ -1420,19 +1420,11 @@ public class FileSystemResourceTest Resource r = base.addPath("//foo.txt"); assertThat("getURI()", r.getURI().toASCIIString(), containsString("//foo.txt")); + + assertThat("isAlias()", r.isAlias(), is(true)); + assertThat("getAlias()", r.getAlias(), notNullValue()); + assertThat("getAlias()", r.getAlias().toASCIIString(), containsString("/foo.txt")); assertThat("Exists: " + r, r.exists(), is(true)); - - if (PathResource.class.isAssignableFrom(resourceClass)) - { - assertThat("isAlias()", r.isAlias(), is(false)); - assertThat("getAlias()", r.getAlias(), nullValue()); - } - else - { - assertThat("isAlias()", r.isAlias(), is(true)); - assertThat("getAlias()", r.getAlias(), notNullValue()); - assertThat("getAlias()", r.getAlias().toASCIIString(), containsString("/foo.txt")); - } } catch (InvalidPathException e) { @@ -1461,19 +1453,10 @@ public class FileSystemResourceTest Resource r = base.addPath("aa//foo.txt"); assertThat("getURI()", r.getURI().toASCIIString(), containsString("aa//foo.txt")); - - if (PathResource.class.isAssignableFrom(resourceClass)) - { - assertThat("isAlias()", r.isAlias(), is(false)); - assertThat("getAlias()", r.getAlias(), nullValue()); - } - else - { - assertThat("isAlias()", r.isAlias(), is(true)); - assertThat("getAlias()", r.getAlias(), notNullValue()); - assertThat("getAlias()", r.getAlias().toASCIIString(), containsString("aa/foo.txt")); - } - + + assertThat("isAlias()", r.isAlias(), is(true)); + assertThat("getAlias()", r.getAlias(), notNullValue()); + assertThat("getAlias()", r.getAlias().toASCIIString(), containsString("aa/foo.txt")); assertThat("Exists: " + r, r.exists(), is(true)); } catch (InvalidPathException e) From 4ab0be9b1bd2686132682908208b385898fc37bd Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Wed, 21 Aug 2019 12:43:25 -0500 Subject: [PATCH 09/55] Issue #3983 - Applying PR review to JarFileResource Signed-off-by: Joakim Erdfelt --- .../eclipse/jetty/util/resource/JarFileResource.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/JarFileResource.java b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/JarFileResource.java index 3ceb0a55cd4..cf6252f6bc1 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/JarFileResource.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/JarFileResource.java @@ -115,11 +115,10 @@ public class JarFileResource extends JarResource _jarFile = null; _list = null; - // Work with encoded URL path - String urlFilePath = _url.getPath(); - int sep = urlFilePath.lastIndexOf("!/"); - _jarUrl = urlFilePath.substring(0, sep + 2); - _path = URIUtil.decodePath(urlFilePath.substring(sep + 2)); + // Work with encoded URL path (_urlString is assumed to be encoded) + int sep = _urlString.lastIndexOf("!/"); + _jarUrl = _urlString.substring(0, sep + 2); + _path = URIUtil.decodePath(_urlString.substring(sep + 2)); if (_path.length() == 0) _path = null; _jarFile = _jarConnection.getJarFile(); @@ -130,7 +129,6 @@ public class JarFileResource extends JarResource * Returns true if the represented resource exists. */ @Override - public boolean exists() { if (_exists) From c1c241349e76f38588c372723abe85dd13522094 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Wed, 21 Aug 2019 12:54:26 -0500 Subject: [PATCH 10/55] Issue #3985 - Applying PR Review to CookieCutter Signed-off-by: Joakim Erdfelt --- .../eclipse/jetty/server/CookieCutter.java | 31 ++++++++++++------- .../server/CookieCutter_LenientTest.java | 2 +- .../org/eclipse/jetty/server/RequestTest.java | 4 ++- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java b/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java index 148e55ebe9c..09fc0614e2a 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java @@ -314,11 +314,7 @@ public class CookieCutter if (_compliance == CookieCompliance.RFC6265) { - // Rejected cookie-octet characters - // US-ASCII characters excluding CTLs, - // whitespace DQUOTE, comma, semicolon, - // and backslash - if (Character.isISOControl(c) || Character.isWhitespace(c) || c == '\\') + if (isRFC6265RejectedCharacter(c)) { reject = true; } @@ -372,12 +368,7 @@ public class CookieCutter if (_compliance == CookieCompliance.RFC6265) { - // Rejected cookie-octet characters - // US-ASCII characters excluding CTLs, - // whitespace DQUOTE, comma, semicolon, - // and backslash - if (Character.isISOControl(c) || Character.isWhitespace(c) || - c == ',' || c == '\\') + if (isRFC6265RejectedCharacter(c)) { reject = true; } @@ -396,4 +387,22 @@ public class CookieCutter _cookies = cookies.toArray(new Cookie[0]); _lastCookies = _cookies; } + + protected boolean isRFC6265RejectedCharacter(char c) + { + // We only reject if a Control Character is encountered + if (Character.isISOControl(c)) + { + return true; + } + + /* TODO: Should we also reject for the complete list of invalid characters in RFC6265? + * + * US-ASCII characters excluding CTLs, + * whitespace DQUOTE, comma, semicolon, + * and backslash + */ + + return false; + } } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutter_LenientTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutter_LenientTest.java index 3f4ba333d96..24272efd981 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutter_LenientTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutter_LenientTest.java @@ -130,7 +130,7 @@ public class CookieCutter_LenientTest Arguments.of("foo=\"bar;baz\"", "foo", "bar;baz"), Arguments.of("z=a;b,c:d;e/f[g]", "z", "a"), Arguments.of("z=\"a;b,c:d;e/f[g]\"", "z", "a;b,c:d;e/f[g]"), - Arguments.of("name=quoted=\"\\\"badly\\\"\"", null, null), // someone attempting to escape a DQUOTE from within a DQUOTED pair) + Arguments.of("name=quoted=\"\\\"badly\\\"\"", "name", "quoted=\"\\\"badly\\\"\""), // someone attempting to escape a DQUOTE from within a DQUOTED pair) // Quoted with other Cookie keywords Arguments.of("x=\"$Version=0\"", "x", "$Version=0"), diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java index 5089ce9593b..69a29dc6b71 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java @@ -1515,7 +1515,9 @@ public class RequestTest "\n" ); assertTrue(response.startsWith("HTTP/1.1 200 OK")); - assertEquals(0, cookies.size()); // this is an invalid cookie + assertEquals(1, cookies.size()); + assertEquals("name", cookies.get(0).getName()); + assertEquals("quoted=\"\\\"badly\\\"\"", cookies.get(0).getValue()); cookies.clear(); response = _connector.getResponse( From 88e37b177c246e383aa3bbc9c651a6135e2d4530 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Wed, 21 Aug 2019 17:11:44 -0500 Subject: [PATCH 11/55] Issue #4000 - Name change to isSameName Signed-off-by: Joakim Erdfelt --- .../jetty/server/handler/AllowSymLinkAliasChecker.java | 2 +- .../java/org/eclipse/jetty/util/resource/PathResource.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AllowSymLinkAliasChecker.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AllowSymLinkAliasChecker.java index 6bebff65c4b..c1dc168f9fa 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AllowSymLinkAliasChecker.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AllowSymLinkAliasChecker.java @@ -52,7 +52,7 @@ public class AllowSymLinkAliasChecker implements AliasCheck Path path = pathResource.getPath(); Path alias = pathResource.getAliasPath(); - if (PathResource.isSame(alias, path)) + if (PathResource.isSameName(alias, path)) return false; // Unknown why this is an alias if (hasSymbolicLink(path) && Files.isSameFile(path, alias)) diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java index c6ba1d3fe04..6892877886d 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java @@ -104,7 +104,7 @@ public class PathResource extends Resource { Path real = abs.toRealPath(FOLLOW_LINKS); - if (!isSame(abs, real)) + if (!isSameName(abs, real)) { return real; } @@ -122,7 +122,7 @@ public class PathResource extends Resource } /** - * Test if the paths are the same. + * Test if the paths are the same name. * *

* If the real path is not the same as the absolute path @@ -175,7 +175,7 @@ public class PathResource extends Resource * in implementation to a.equals(b) *

*/ - public static boolean isSame(Path pathA, Path pathB) + public static boolean isSameName(Path pathA, Path pathB) { int aCount = pathA.getNameCount(); int bCount = pathB.getNameCount(); From 2cbdb27e5b20f20c1fa32e091ce61bc05ab14521 Mon Sep 17 00:00:00 2001 From: olivier lamy Date: Thu, 22 Aug 2019 20:51:34 +1000 Subject: [PATCH 12/55] touch to test github/jenkins Signed-off-by: olivier lamy --- Jenkinsfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index da0aa068230..e726b7eef00 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -111,7 +111,6 @@ pipeline { } } - def slackNotif() { script { try @@ -132,7 +131,6 @@ def slackNotif() { } } - /** * To other developers, if you are using this method above, please use the following syntax. * From 595e058fa317c71f9b94ba3be0ce6c9bc3f97785 Mon Sep 17 00:00:00 2001 From: olivier lamy Date: Thu, 22 Aug 2019 20:53:46 +1000 Subject: [PATCH 13/55] touch to test github/jenkins Signed-off-by: olivier lamy --- Jenkinsfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Jenkinsfile b/Jenkinsfile index e726b7eef00..554ea76bf1f 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -156,4 +156,5 @@ def mavenBuild(jdk, cmdline, mvnName, junitPublishDisabled) { } } + // vim: et:ts=2:sw=2:ft=groovy From 7f5aa89042ab026ada44bfa2f73485c0fc71e5b4 Mon Sep 17 00:00:00 2001 From: Alexander Kurtakov Date: Thu, 22 Aug 2019 16:06:06 +0300 Subject: [PATCH 14/55] Update Tycho and Eclipse CBI plugins. * Tycho to 1.4.0 * CBI to 1.1.7 These updates are important for both speed and m2e compatibility so warnings are not shown in the eclipse ide. Signed-off-by: Alexander Kurtakov --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index d56e0036994..e717c2a81c1 100644 --- a/pom.xml +++ b/pom.xml @@ -31,8 +31,8 @@ 7.1 1.21 benchmarks - 1.2.0 - 1.1.5 + 1.4.0 + 1.1.7 5.5.1 3.6.0 1.3.1 From 318045cd876dd4fb050ee980a601d5e81ba1ab87 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Thu, 22 Aug 2019 12:46:23 -0500 Subject: [PATCH 15/55] Issue #3985 - Applying PR Review to CookieCutter Signed-off-by: Joakim Erdfelt --- .../eclipse/jetty/server/CookieCutter.java | 35 ++++++++++++------- .../server/CookieCutter_LenientTest.java | 2 +- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java b/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java index 09fc0614e2a..850f07ccef2 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java @@ -314,7 +314,7 @@ public class CookieCutter if (_compliance == CookieCompliance.RFC6265) { - if (isRFC6265RejectedCharacter(c)) + if (isRFC6265RejectedCharacter(inQuoted, c)) { reject = true; } @@ -368,7 +368,7 @@ public class CookieCutter if (_compliance == CookieCompliance.RFC6265) { - if (isRFC6265RejectedCharacter(c)) + if (isRFC6265RejectedCharacter(inQuoted, c)) { reject = true; } @@ -388,20 +388,29 @@ public class CookieCutter _lastCookies = _cookies; } - protected boolean isRFC6265RejectedCharacter(char c) + protected boolean isRFC6265RejectedCharacter(boolean inQuoted, char c) { - // We only reject if a Control Character is encountered - if (Character.isISOControl(c)) + if (inQuoted) { - return true; + // We only reject if a Control Character is encountered + if (Character.isISOControl(c)) + { + return true; + } + } + else + { + /* From RFC6265 - Section 4.1.1 - Syntax + * cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E + * ; US-ASCII characters excluding CTLs, + * ; whitespace DQUOTE, comma, semicolon, + * ; and backslash + */ + return Character.isISOControl(c) || // control characters + c > 127 || // 8-bit characters + c == ',' || // comma + c == ';'; // semicolon } - - /* TODO: Should we also reject for the complete list of invalid characters in RFC6265? - * - * US-ASCII characters excluding CTLs, - * whitespace DQUOTE, comma, semicolon, - * and backslash - */ return false; } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutter_LenientTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutter_LenientTest.java index 24272efd981..44c5234217c 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutter_LenientTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutter_LenientTest.java @@ -116,7 +116,7 @@ public class CookieCutter_LenientTest Arguments.of("x=\"abc\\", "x", "\"abc\\"), // UTF-8 raw values (not encoded) - VIOLATION of RFC6265 - Arguments.of("2sides=\u262F", "2sides", "\u262f"), // 2 byte (YIN YANG) + Arguments.of("2sides=\u262F", null, null), // 2 byte (YIN YANG) - rejected due to not being DQUOTED Arguments.of("currency=\"\u20AC\"", "currency", "\u20AC"), // 3 byte (EURO SIGN) Arguments.of("gothic=\"\uD800\uDF48\"", "gothic", "\uD800\uDF48"), // 4 byte (GOTHIC LETTER HWAIR) From e1656aecc797e5385f0f44deac566418613cb44c Mon Sep 17 00:00:00 2001 From: olivier lamy Date: Fri, 23 Aug 2019 06:58:54 +1000 Subject: [PATCH 16/55] fix compilation Signed-off-by: olivier lamy --- .../org/eclipse/jetty/util/resource/JarResourceTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/resource/JarResourceTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/resource/JarResourceTest.java index 1fc6f4afe5e..da57eda1587 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/resource/JarResourceTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/resource/JarResourceTest.java @@ -196,7 +196,7 @@ public class JarResourceTest Path testJar = MavenTestingUtils.getTestResourcePathFile("jar-file-resource.jar"); String uri = "jar:" + testJar.toUri().toASCIIString() + "!/"; - Resource resource = new JarFileResource(URI.create(uri).toURL()); + Resource resource = new JarFileResource(URI.create(uri).toURL(),false); Resource rez = resource.addPath("rez/"); assertThat("path /rez/ is a dir", rez.isDirectory(), is(true)); @@ -224,7 +224,7 @@ public class JarResourceTest Path testJar = MavenTestingUtils.getTestResourcePathFile("jar-file-resource.jar"); String uri = "jar:" + testJar.toUri().toASCIIString() + "!/"; - Resource resource = new JarFileResource(URI.create(uri).toURL()); + Resource resource = new JarFileResource(URI.create(uri).toURL(),false); Resource rez = resource.addPath("rez/oddities/"); assertThat("path /rez/oddities/ is a dir", rez.isDirectory(), is(true)); @@ -248,7 +248,7 @@ public class JarResourceTest Path testJar = MavenTestingUtils.getTestResourcePathFile("jar-file-resource.jar"); String uri = "jar:" + testJar.toUri().toASCIIString() + "!/"; - Resource resource = new JarFileResource(URI.create(uri).toURL()); + Resource resource = new JarFileResource(URI.create(uri).toURL(),false); Resource anotherDir = resource.addPath("rez/another dir/"); assertThat("path /rez/another dir/ is a dir", anotherDir.isDirectory(), is(true)); From 5bcbe0f9d9ab83c14584b63ef2ffe18579f89f9e Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Thu, 22 Aug 2019 16:45:04 -0500 Subject: [PATCH 17/55] Adding javax.websocket secure client example Signed-off-by: Joakim Erdfelt --- .../src/test/java/examples/EchoEndpoint.java | 90 +++++++++++++ .../examples/OriginServerConfigurator.java | 44 +++++++ .../SecureWebSocketContainerExample.java | 121 ++++++++++++++++++ .../examples/jetty-websocket-httpclient.xml | 22 ++++ 4 files changed, 277 insertions(+) create mode 100644 jetty-websocket/javax-websocket-client-impl/src/test/java/examples/EchoEndpoint.java create mode 100644 jetty-websocket/javax-websocket-client-impl/src/test/java/examples/OriginServerConfigurator.java create mode 100644 jetty-websocket/javax-websocket-client-impl/src/test/java/examples/SecureWebSocketContainerExample.java create mode 100644 jetty-websocket/javax-websocket-client-impl/src/test/resources/examples/jetty-websocket-httpclient.xml diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/examples/EchoEndpoint.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/examples/EchoEndpoint.java new file mode 100644 index 00000000000..cea500b23bf --- /dev/null +++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/examples/EchoEndpoint.java @@ -0,0 +1,90 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package examples; + +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import javax.websocket.CloseReason; +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfig; +import javax.websocket.MessageHandler; +import javax.websocket.Session; + +/** + * Basic Echo Client Endpoint + */ +public class EchoEndpoint extends Endpoint implements MessageHandler.Whole +{ + private final CountDownLatch closeLatch = new CountDownLatch(1); + private Session session; + + public boolean awaitClose(int duration, TimeUnit unit) throws InterruptedException + { + return this.closeLatch.await(duration, unit); + } + + @Override + public void onClose(Session session, CloseReason closeReason) + { + System.out.printf("Connection closed: Session.id=%s - %s%n", session.getId(), closeReason); + this.session = null; + this.closeLatch.countDown(); // trigger latch + } + + @Override + public void onOpen(Session session, EndpointConfig config) + { + System.out.printf("Got open: Session.id=%s%n", session.getId()); + this.session = session; + this.session.addMessageHandler(this); + try + { + session.getBasicRemote().sendText("Hello"); + session.getBasicRemote().sendText("Thanks for the conversation."); + } + catch (Throwable t) + { + t.printStackTrace(); + } + } + + @Override + public void onMessage(String msg) + { + System.out.printf("Got msg: \"%s\"%n", msg); + if (msg.contains("Thanks")) + { + try + { + session.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "I'm done")); + } + catch (IOException e) + { + e.printStackTrace(); + } + } + } + + @Override + public void onError(Session session, Throwable cause) + { + cause.printStackTrace(); + } +} diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/examples/OriginServerConfigurator.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/examples/OriginServerConfigurator.java new file mode 100644 index 00000000000..cf08984074f --- /dev/null +++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/examples/OriginServerConfigurator.java @@ -0,0 +1,44 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package examples; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import javax.websocket.ClientEndpointConfig; + +/** + * Provide a means to set the `Origin` header for outgoing WebSocket upgrade requests + */ +public class OriginServerConfigurator extends ClientEndpointConfig.Configurator +{ + private final String originServer; + + public OriginServerConfigurator(String originServer) + { + this.originServer = originServer; + } + + @Override + public void beforeRequest(Map> headers) + { + headers.put("Origin", Collections.singletonList(originServer)); + super.beforeRequest(headers); + } +} diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/examples/SecureWebSocketContainerExample.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/examples/SecureWebSocketContainerExample.java new file mode 100644 index 00000000000..c70fc4ffe0b --- /dev/null +++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/examples/SecureWebSocketContainerExample.java @@ -0,0 +1,121 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package examples; + +import java.io.FileNotFoundException; +import java.net.URI; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.concurrent.TimeUnit; +import javax.websocket.ClientEndpointConfig; +import javax.websocket.ContainerProvider; +import javax.websocket.WebSocketContainer; + +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.thread.ThreadClassLoaderScope; + +public class SecureWebSocketContainerExample +{ + public static void main(String[] args) + { + String destUri = "wss://echo.websocket.org"; + if (args.length > 0) + { + destUri = args[0]; + } + + WebSocketContainer client = null; + try + { + client = getConfiguredWebSocketContainer(); + URI echoUri = new URI(destUri); + + ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create() + .configurator(new OriginServerConfigurator("https://websocket.org")) + .build(); + EchoEndpoint echoEndpoint = new EchoEndpoint(); + client.connectToServer(echoEndpoint, clientEndpointConfig, echoUri); + System.out.printf("Connecting to : %s%n", echoUri); + + // wait for closed socket connection. + echoEndpoint.awaitClose(5, TimeUnit.SECONDS); + } + catch (Throwable t) + { + t.printStackTrace(); + } + finally + { + /* Since javax.websocket clients have no defined LifeCycle we + * want to either close/stop the client, or exit the JVM + * via a System.exit(), otherwise the threads this client keeps + * open will prevent the JVM from terminating naturally. + */ + LifeCycle.stop(client); + } + } + + /** + * Since javax.websocket does not have an API for configuring SSL, each implementation + * of javax.websocket has to come up with their own SSL configuration mechanism. + *

+ * When the call to {@link javax.websocket.ContainerProvider}.{@link ContainerProvider#getWebSocketContainer()} + * occurs, that needs to have a started and available WebSocket Client. + * Jetty's {@code WebSocketClient} must have a Jetty {@code HttpClient} started as well. + * If you want SSL, then that configuration has to be passed into the Jetty {@code HttpClient} at initialization. + *

+ *

+ * How Jetty makes this available, is via the {@code jetty-websocket-httpclient.xml} classloader resource + * along with the jetty-xml artifact. + *

+ *

+ * This method will look for the file in the classloader resources, and then + * sets up a {@link URLClassLoader} to make that {@code jetty-websocket-httpclient.xml} available + * for this specific example. + * If we had put the `jetty-websocket-httpclient.xml` in the root of a JAR file loaded by this + * project then you can skip all of the classloader trickery this method performs. + *

+ * + * @return the client WebSocketContainer + * @see javax.websocket issue #210 + */ + public static WebSocketContainer getConfiguredWebSocketContainer() throws Exception + { + URL jettyHttpClientConfigUrl = Thread.currentThread().getContextClassLoader() + .getResource("examples/jetty-websocket-httpclient.xml"); + + if (jettyHttpClientConfigUrl == null) + { + throw new FileNotFoundException("Unable to find Jetty HttpClient configuration XML"); + } + + URI jettyConfigDirUri = jettyHttpClientConfigUrl.toURI().resolve("./"); + + ClassLoader parentClassLoader = Thread.currentThread().getContextClassLoader(); + URL[] urls = new URL[]{ + jettyConfigDirUri.toURL() + }; + URLClassLoader classLoader = new URLClassLoader(urls, parentClassLoader); + + try (ThreadClassLoaderScope ignore = new ThreadClassLoaderScope(classLoader)) + { + return ContainerProvider.getWebSocketContainer(); + } + } +} diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/resources/examples/jetty-websocket-httpclient.xml b/jetty-websocket/javax-websocket-client-impl/src/test/resources/examples/jetty-websocket-httpclient.xml new file mode 100644 index 00000000000..672007a92cd --- /dev/null +++ b/jetty-websocket/javax-websocket-client-impl/src/test/resources/examples/jetty-websocket-httpclient.xml @@ -0,0 +1,22 @@ + + + + + + false + + + + TLS/1.3 + + + + + + + + + + + 5000 + From 6aaf22ec6cc86431536c09e48ed59fe14e5ebf2c Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Thu, 22 Aug 2019 17:02:19 -0500 Subject: [PATCH 18/55] Merge branch `jetty-9.4.x` into `jetty-10.0.x` Signed-off-by: Joakim Erdfelt --- .../src/test/java/examples/EchoEndpoint.java | 0 .../src/test/java/examples/OriginServerConfigurator.java | 0 .../src/test/java/examples/SecureWebSocketContainerExample.java | 1 + .../src/test/resources/examples/jetty-websocket-httpclient.xml | 0 4 files changed, 1 insertion(+) rename jetty-websocket/{javax-websocket-client-impl => javax-websocket-client}/src/test/java/examples/EchoEndpoint.java (100%) rename jetty-websocket/{javax-websocket-client-impl => javax-websocket-client}/src/test/java/examples/OriginServerConfigurator.java (100%) rename jetty-websocket/{javax-websocket-client-impl => javax-websocket-client}/src/test/java/examples/SecureWebSocketContainerExample.java (98%) rename jetty-websocket/{javax-websocket-client-impl => javax-websocket-client}/src/test/resources/examples/jetty-websocket-httpclient.xml (100%) diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/examples/EchoEndpoint.java b/jetty-websocket/javax-websocket-client/src/test/java/examples/EchoEndpoint.java similarity index 100% rename from jetty-websocket/javax-websocket-client-impl/src/test/java/examples/EchoEndpoint.java rename to jetty-websocket/javax-websocket-client/src/test/java/examples/EchoEndpoint.java diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/examples/OriginServerConfigurator.java b/jetty-websocket/javax-websocket-client/src/test/java/examples/OriginServerConfigurator.java similarity index 100% rename from jetty-websocket/javax-websocket-client-impl/src/test/java/examples/OriginServerConfigurator.java rename to jetty-websocket/javax-websocket-client/src/test/java/examples/OriginServerConfigurator.java diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/examples/SecureWebSocketContainerExample.java b/jetty-websocket/javax-websocket-client/src/test/java/examples/SecureWebSocketContainerExample.java similarity index 98% rename from jetty-websocket/javax-websocket-client-impl/src/test/java/examples/SecureWebSocketContainerExample.java rename to jetty-websocket/javax-websocket-client/src/test/java/examples/SecureWebSocketContainerExample.java index c70fc4ffe0b..50f2c3fecda 100644 --- a/jetty-websocket/javax-websocket-client-impl/src/test/java/examples/SecureWebSocketContainerExample.java +++ b/jetty-websocket/javax-websocket-client/src/test/java/examples/SecureWebSocketContainerExample.java @@ -66,6 +66,7 @@ public class SecureWebSocketContainerExample * want to either close/stop the client, or exit the JVM * via a System.exit(), otherwise the threads this client keeps * open will prevent the JVM from terminating naturally. + * @see https://github.com/eclipse-ee4j/websocket-api/issues/212 */ LifeCycle.stop(client); } diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/resources/examples/jetty-websocket-httpclient.xml b/jetty-websocket/javax-websocket-client/src/test/resources/examples/jetty-websocket-httpclient.xml similarity index 100% rename from jetty-websocket/javax-websocket-client-impl/src/test/resources/examples/jetty-websocket-httpclient.xml rename to jetty-websocket/javax-websocket-client/src/test/resources/examples/jetty-websocket-httpclient.xml From 12e1473fd0617e4d2c69d6e0864d119fc7dd75b7 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Thu, 22 Aug 2019 17:04:01 -0500 Subject: [PATCH 19/55] Adding reference to javax.websocket issue Signed-off-by: Joakim Erdfelt --- .../src/test/java/examples/SecureWebSocketContainerExample.java | 1 + 1 file changed, 1 insertion(+) diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/examples/SecureWebSocketContainerExample.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/examples/SecureWebSocketContainerExample.java index c70fc4ffe0b..50f2c3fecda 100644 --- a/jetty-websocket/javax-websocket-client-impl/src/test/java/examples/SecureWebSocketContainerExample.java +++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/examples/SecureWebSocketContainerExample.java @@ -66,6 +66,7 @@ public class SecureWebSocketContainerExample * want to either close/stop the client, or exit the JVM * via a System.exit(), otherwise the threads this client keeps * open will prevent the JVM from terminating naturally. + * @see https://github.com/eclipse-ee4j/websocket-api/issues/212 */ LifeCycle.stop(client); } From 80d513e97971dc983e6402c62499ad871280ecc6 Mon Sep 17 00:00:00 2001 From: olivier lamy Date: Fri, 23 Aug 2019 10:55:57 +1000 Subject: [PATCH 20/55] junit failure after bad merge Signed-off-by: olivier lamy --- .../test/java/org/eclipse/jetty/server/PartialRFC2616Test.java | 1 - 1 file changed, 1 deletion(-) diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/PartialRFC2616Test.java b/jetty-server/src/test/java/org/eclipse/jetty/server/PartialRFC2616Test.java index fce7d80d099..47c62ec54bf 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/PartialRFC2616Test.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/PartialRFC2616Test.java @@ -317,7 +317,6 @@ public class PartialRFC2616Test "Content-Length: 5\n" + "\n" + "123\r\n" + - "123\015\012" + "GET /R2 HTTP/1.1\n" + "Host: localhost\n" + "Transfer-Encoding: other\n" + From cf592469aac9c9484207142579648407327f278f Mon Sep 17 00:00:00 2001 From: olivier lamy Date: Fri, 23 Aug 2019 13:09:15 +1000 Subject: [PATCH 21/55] checkstyle fixes. Signed-off-by: olivier lamy --- .../javax/tests/client/WriteTimeoutTest.java | 6 ++-- .../javax/tests/coders/TimeEncoder.java | 2 +- .../tests/server/PrimitivesTextEchoTest.java | 4 +-- .../jetty/websocket/core/ParserTest.java | 12 +++---- .../jetty/websocket/core/RawFrameBuilder.java | 6 ++-- .../websocket/core/TestAsyncFrameHandler.java | 2 +- .../websocket/core/TestMessageHandler.java | 2 +- .../PerMessageDeflateExtensionTest.java | 32 +++++++++---------- .../websocket/core/proxy/WebSocketProxy.java | 8 ++--- .../core/proxy/WebSocketProxyTest.java | 3 +- 10 files changed, 38 insertions(+), 39 deletions(-) diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/client/WriteTimeoutTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/client/WriteTimeoutTest.java index 9aa3634c89f..5f67ffb7331 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/client/WriteTimeoutTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/client/WriteTimeoutTest.java @@ -107,18 +107,18 @@ public class WriteTimeoutTest @ServerEndpoint("/logSocket") public static class LoggingSocket { - private final Logger LOG = Log.getLogger(LoggingSocket.class); + private final Logger log = Log.getLogger(LoggingSocket.class); @OnMessage public void onMessage(String msg) { - LOG.debug("onMessage(): {}", msg); + log.debug("onMessage(): {}", msg); } @OnError public void onError(Throwable t) { - LOG.debug("onError(): {}", t); + log.debug("onError(): {}", t); } } } diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/TimeEncoder.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/TimeEncoder.java index e482566b249..f4f9c2a4e4c 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/TimeEncoder.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/TimeEncoder.java @@ -30,7 +30,7 @@ import javax.websocket.EndpointConfig; */ public class TimeEncoder implements Encoder.Text { - private TimeZone GMT = TimeZone.getTimeZone("GMT"); + private static final TimeZone GMT = TimeZone.getTimeZone("GMT"); @Override public String encode(Date object) throws EncodeException diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/PrimitivesTextEchoTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/PrimitivesTextEchoTest.java index a01395e264b..6f8fdbe7919 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/PrimitivesTextEchoTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/PrimitivesTextEchoTest.java @@ -324,14 +324,14 @@ public class PrimitivesTextEchoTest addCase(data, LongEchoSocket.class, Long.toString(0), "0"); addCase(data, LongEchoSocket.class, Long.toString(100_000), "100000"); addCase(data, LongEchoSocket.class, Long.toString(-2_000_000), "-2000000"); - addCase(data, LongEchoSocket.class, Long.toString(300_000_000_000l), "300000000000"); + addCase(data, LongEchoSocket.class, Long.toString(300_000_000_000L), "300000000000"); addCase(data, LongEchoSocket.class, Long.toString(Long.MAX_VALUE), Long.toString(Long.MAX_VALUE)); addCase(data, LongEchoSocket.class, Long.toString(Long.MIN_VALUE), Long.toString(Long.MIN_VALUE)); addCase(data, LongObjEchoSocket.class, Long.toString(0), "0"); addCase(data, LongObjEchoSocket.class, Long.toString(100_000), "100000"); addCase(data, LongObjEchoSocket.class, Long.toString(-2_000_000), "-2000000"); - addCase(data, LongObjEchoSocket.class, Long.toString(300_000_000_000l), "300000000000"); + addCase(data, LongObjEchoSocket.class, Long.toString(300_000_000_000L), "300000000000"); addCase(data, LongObjEchoSocket.class, Long.toString(Long.MAX_VALUE), Long.toString(Long.MAX_VALUE)); addCase(data, LongObjEchoSocket.class, Long.toString(Long.MIN_VALUE), Long.toString(Long.MIN_VALUE)); diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/ParserTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/ParserTest.java index f1eac07f18f..f267a0e8549 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/ParserTest.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/ParserTest.java @@ -98,7 +98,7 @@ public class ParserTest * From Autobahn WebSocket Server Testcase 1.2.2 */ @Test - public void testParse_Binary_125BytePayload() throws InterruptedException + public void testParseBinary125BytePayload() throws InterruptedException { int length = 125; @@ -128,7 +128,7 @@ public class ParserTest * From Autobahn WebSocket Server Testcase 1.2.3 */ @Test - public void testParse_Binary_126BytePayload() throws InterruptedException + public void testParseBinary126BytePayload() throws InterruptedException { int length = 126; @@ -161,7 +161,7 @@ public class ParserTest * From Autobahn WebSocket Server Testcase 1.2.4 */ @Test - public void testParse_Binary_127BytePayload() throws InterruptedException + public void testParseBinary127BytePayload() throws InterruptedException { int length = 127; @@ -193,7 +193,7 @@ public class ParserTest * From Autobahn WebSocket Server Testcase 1.2.5 */ @Test - public void testParse_Binary_128BytePayload() throws InterruptedException + public void testParseBinary128BytePayload() throws InterruptedException { int length = 128; @@ -225,7 +225,7 @@ public class ParserTest * From Autobahn WebSocket Server Testcase 1.2.6 */ @Test - public void testParse_Binary_65535BytePayload() throws InterruptedException + public void testParseBinary65535BytePayload() throws InterruptedException { int length = 65535; @@ -257,7 +257,7 @@ public class ParserTest * From Autobahn WebSocket Server Testcase 1.2.7 */ @Test - public void testParse_Binary_65536BytePayload() throws InterruptedException + public void testParseBinary65536BytePayload() throws InterruptedException { int length = 65536; diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/RawFrameBuilder.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/RawFrameBuilder.java index f498d314a3b..5030174ff75 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/RawFrameBuilder.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/RawFrameBuilder.java @@ -39,7 +39,7 @@ public class RawFrameBuilder buf.put(b); } - public static void putLengthAndMask(ByteBuffer buf, int length, byte mask[]) + public static void putLengthAndMask(ByteBuffer buf, int length, byte[] mask) { if (mask != null) { @@ -53,7 +53,7 @@ public class RawFrameBuilder } } - public static void mask(final byte[] data, final byte mask[]) + public static void mask(final byte[] data, final byte[] mask) { assertThat("Mask.length", mask.length, is(4)); int len = data.length; @@ -96,7 +96,7 @@ public class RawFrameBuilder } } - public static void putMask(ByteBuffer buf, byte mask[]) + public static void putMask(ByteBuffer buf, byte[] mask) { assertThat("Mask.length", mask.length, is(4)); buf.put(mask); diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/TestAsyncFrameHandler.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/TestAsyncFrameHandler.java index faac67ec21b..b63186e27cd 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/TestAsyncFrameHandler.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/TestAsyncFrameHandler.java @@ -76,7 +76,7 @@ public class TestAsyncFrameHandler implements FrameHandler @Override public void onError(Throwable cause, Callback callback) { - LOG.info("[{}] onError {} ", name, cause == null?null:cause.toString()); + LOG.info("[{}] onError {} ", name, cause == null ? null : cause.toString()); error = cause; errorLatch.countDown(); callback.succeeded(); diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/TestMessageHandler.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/TestMessageHandler.java index 448eef59a30..99b95f3dd9b 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/TestMessageHandler.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/TestMessageHandler.java @@ -29,7 +29,7 @@ import org.eclipse.jetty.util.log.Logger; public class TestMessageHandler extends MessageHandler { - protected final Logger LOG = Log.getLogger(TestMessageHandler.class); + protected static final Logger LOG = Log.getLogger(TestMessageHandler.class); public CoreSession coreSession; public BlockingQueue textMessages = new BlockingArrayQueue<>(); diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/PerMessageDeflateExtensionTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/PerMessageDeflateExtensionTest.java index e9e0e499368..71a4d35ea42 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/PerMessageDeflateExtensionTest.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/PerMessageDeflateExtensionTest.java @@ -79,7 +79,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest * Section 8.2.3.1: A message compressed using 1 compressed DEFLATE block */ @Test - public void testDraft21_Hello_UnCompressedBlock() + public void testDraft21HelloUnCompressedBlock() { ExtensionTool.Tester tester = clientExtensions.newTester("permessage-deflate"); @@ -100,7 +100,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest * Section 8.2.3.1: A message compressed using 1 compressed DEFLATE block (with fragmentation) */ @Test - public void testDraft21_Hello_UnCompressedBlock_Fragmented() + public void testDraft21HelloUnCompressedBlockFragmented() { ExtensionTool.Tester tester = clientExtensions.newTester("permessage-deflate"); @@ -123,7 +123,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest * Section 8.2.3.2: Sharing LZ77 Sliding Window */ @Test - public void testDraft21_SharingL77SlidingWindow_ContextTakeover() + public void testDraft21SharingL77SlidingWindowContextTakeover() { ExtensionTool.Tester tester = clientExtensions.newTester("permessage-deflate"); @@ -146,7 +146,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest * Section 8.2.3.2: Sharing LZ77 Sliding Window */ @Test - public void testDraft21_SharingL77SlidingWindow_NoContextTakeover() + public void testDraft21SharingL77SlidingWindowNoContextTakeover() { ExtensionTool.Tester tester = clientExtensions.newTester("permessage-deflate"); @@ -170,7 +170,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest * Section 8.2.3.3: Using a DEFLATE Block with No Compression */ @Test - public void testDraft21_DeflateBlockWithNoCompression() + public void testDraft21DeflateBlockWithNoCompression() { ExtensionTool.Tester tester = clientExtensions.newTester("permessage-deflate"); @@ -189,7 +189,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest * Section 8.2.3.4: Using a DEFLATE Block with BFINAL Set to 1 */ @Test - public void testDraft21_DeflateBlockWithBFinal1() + public void testDraft21DeflateBlockWithBFinal1() { ExtensionTool.Tester tester = clientExtensions.newTester("permessage-deflate"); @@ -209,7 +209,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest * Section 8.2.3.5: Two DEFLATE Blocks in 1 Message */ @Test - public void testDraft21_TwoDeflateBlocksOneMessage() + public void testDraft21TwoDeflateBlocksOneMessage() { ExtensionTool.Tester tester = clientExtensions.newTester("permessage-deflate"); @@ -227,7 +227,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest * Decode fragmented message (3 parts: TEXT, CONTINUATION, CONTINUATION) */ @Test - public void testParseFragmentedMessage_Good() + public void testParseFragmentedMessageGood() { ExtensionTool.Tester tester = clientExtensions.newTester("permessage-deflate"); @@ -256,7 +256,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest *

*/ @Test - public void testParseFragmentedMessage_BadRsv1() + public void testParseFragmentedMessageBadRsv1() { ExtensionTool.Tester tester = clientExtensions.newTester("permessage-deflate"); @@ -489,7 +489,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest } @Test - public void testPyWebSocket_Client_NoContextTakeover_ThreeOra() + public void testPyWebSocketClientNoContextTakeoverThreeOra() { ExtensionTool.Tester tester = clientExtensions.newTester("permessage-deflate; client_max_window_bits; client_no_context_takeover"); @@ -497,7 +497,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest // Captured from Pywebsocket (r790) - 3 messages with similar parts. - tester.parseIncomingHex( // context takeover (3 messages) + tester.parseIncomingHex(// context takeover (3 messages) "c1 09 0a c9 2f 4a 0c 01 62 00 00", // ToraTora "c1 0b 72 2c c9 2f 4a 74 cb 01 12 00 00", // AtoraFlora "c1 0b 0a c8 c8 c9 2f 4a 0c 01 62 00 00" // PhloraTora @@ -507,7 +507,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest } @Test - public void testPyWebSocket_Client_ToraToraTora() + public void testPyWebSocketClientToraToraTora() { ExtensionTool.Tester tester = clientExtensions.newTester("permessage-deflate; client_max_window_bits"); @@ -525,7 +525,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest } @Test - public void testPyWebSocket_Server_NoContextTakeover_ThreeOra() + public void testPyWebSocketServerNoContextTakeoverThreeOra() { ExtensionTool.Tester tester = serverExtensions.newTester("permessage-deflate; client_max_window_bits; client_no_context_takeover"); @@ -533,7 +533,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest // Captured from Pywebsocket (r790) - 3 messages with similar parts. - tester.parseIncomingHex( // context takeover (3 messages) + tester.parseIncomingHex(// context takeover (3 messages) "c1 89 88 bc 1b b1 82 75 34 fb 84 bd 79 b1 88", // ToraTora "c1 8b 50 86 88 b2 22 aa 41 9d 1a f2 43 b3 42 86 88", // AtoraFlora "c1 8b e2 3e 05 53 e8 f6 cd 9a cd 74 09 52 80 3e 05" // PhloraTora @@ -543,7 +543,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest } @Test - public void testPyWebSocket_Server_ToraToraTora() + public void testPyWebSocketServerToraToraTora() { ExtensionTool.Tester tester = serverExtensions.newTester("permessage-deflate; client_max_window_bits"); @@ -551,7 +551,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest // Captured from Pywebsocket (r790) - "tora" sent 3 times. - tester.parseIncomingHex( // context takeover (3 messages) + tester.parseIncomingHex(// context takeover (3 messages) "c1 86 69 39 fe 91 43 f0 d1 db 6d 39", // tora 1 "c1 85 2d f3 eb 96 07 f2 89 96 2d", // tora 2 "c1 84 53 ad a5 34 51 cc a5 34" // tora 3 diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/proxy/WebSocketProxy.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/proxy/WebSocketProxy.java index 1d011d3daa2..24dd71c2f30 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/proxy/WebSocketProxy.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/proxy/WebSocketProxy.java @@ -188,7 +188,7 @@ class WebSocketProxy // the callback is saved until a close response comes in sendFrame from Server2Proxy // if the callback was completed here then core would send its own close response closeCallback = callback; - sendCallback = Callback.from(()->{}, callback::failed); + sendCallback = Callback.from(() -> {}, callback::failed); } break; @@ -383,7 +383,7 @@ class WebSocketProxy try { state = State.CONNECTING; - client.connect(this, serverUri).whenComplete((s,t)-> + client.connect(this, serverUri).whenComplete((s,t) -> { if (t != null) onConnectFailure(t, callback); @@ -526,7 +526,7 @@ class WebSocketProxy { state = State.ISHUT; closeCallback = callback; - sendCallback = Callback.from(()->{}, callback::failed); + sendCallback = Callback.from(() -> {}, callback::failed); } break; @@ -686,4 +686,4 @@ class WebSocketProxy return "Server2Proxy:" + getState(); } } -} \ No newline at end of file +} diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/proxy/WebSocketProxyTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/proxy/WebSocketProxyTest.java index 56111ae6c25..d442d24a0ee 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/proxy/WebSocketProxyTest.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/proxy/WebSocketProxyTest.java @@ -221,7 +221,6 @@ public class WebSocketProxyTest assertThat(closeStatus.getReason(), containsString("Failed to upgrade to websocket: Unexpected HTTP Response Status Code:")); } - @Test public void testClientError() throws Exception { @@ -241,7 +240,7 @@ public class WebSocketProxyTest ClientUpgradeRequest upgradeRequest = ClientUpgradeRequest.from(_client, new URI("ws://localhost:8080/proxy/"), clientFrameHandler); upgradeRequest.setConfiguration(defaultCustomizer); CompletableFuture response = _client.connect(upgradeRequest); - Exception e = assertThrows(ExecutionException.class, ()->response.get(5, TimeUnit.SECONDS)); + Exception e = assertThrows(ExecutionException.class, () -> response.get(5, TimeUnit.SECONDS)); assertThat(e.getMessage(), containsString("simulated client onOpen error")); assertTrue(clientFrameHandler.closeLatch.await(5, TimeUnit.SECONDS)); assertTrue(serverFrameHandler.closeLatch.await(5, TimeUnit.SECONDS)); From f692af3d539c63ea455359a5b93537653dd4aba6 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Fri, 23 Aug 2019 11:55:13 -0500 Subject: [PATCH 22/55] Issue #155 - Adding public ClientContainer(HttpClient) constructor + This helps with programmatic configuration of HttpClient layer (for SSL and Proxy). Signed-off-by: Joakim Erdfelt --- .../websocket/jsr356/ClientContainer.java | 12 +++ .../SecureClientContainerExample.java | 84 +++++++++++++++++++ .../SecureWebSocketContainerExample.java | 9 +- 3 files changed, 97 insertions(+), 8 deletions(-) create mode 100644 jetty-websocket/javax-websocket-client-impl/src/test/java/examples/SecureClientContainerExample.java diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/ClientContainer.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/ClientContainer.java index b1014515a58..0e1bb7f1e72 100644 --- a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/ClientContainer.java +++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/ClientContainer.java @@ -114,6 +114,18 @@ public class ClientContainer extends ContainerLifeCycle implements WebSocketCont client.addManaged(client.getHttpClient()); } + /** + * Create a {@link javax.websocket.WebSocketContainer} using the supplied + * {@link HttpClient} for environments where you want to configure + * SSL/TLS or Proxy behaviors. + * + * @param httpClient the HttpClient instance to use + */ + public ClientContainer(final HttpClient httpClient) + { + this(new SimpleContainerScope(WebSocketPolicy.newClientPolicy()), httpClient); + } + /** * This is the entry point for ServerContainer, via ServletContext.getAttribute(ServerContainer.class.getName()) * diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/examples/SecureClientContainerExample.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/examples/SecureClientContainerExample.java new file mode 100644 index 00000000000..dbba5d2365c --- /dev/null +++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/examples/SecureClientContainerExample.java @@ -0,0 +1,84 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package examples; + +import java.net.URI; +import java.util.concurrent.TimeUnit; +import javax.websocket.ClientEndpointConfig; +import javax.websocket.WebSocketContainer; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.websocket.jsr356.ClientContainer; + +public class SecureClientContainerExample +{ + public static void main(String[] args) + { + WebSocketContainer client = null; + try + { + URI echoUri = URI.create("wss://echo.websocket.org"); + client = getConfiguredWebSocketContainer(); + + ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create() + .configurator(new OriginServerConfigurator("https://websocket.org")) + .build(); + EchoEndpoint echoEndpoint = new EchoEndpoint(); + client.connectToServer(echoEndpoint, clientEndpointConfig, echoUri); + System.out.printf("Connecting to : %s%n", echoUri); + + // wait for closed socket connection. + echoEndpoint.awaitClose(5, TimeUnit.SECONDS); + } + catch (Throwable t) + { + t.printStackTrace(); + } + finally + { + /* Since javax.websocket clients have no defined LifeCycle we + * want to either close/stop the client, or exit the JVM + * via a System.exit(), otherwise the threads this client keeps + * open will prevent the JVM from terminating naturally. + * @see https://github.com/eclipse-ee4j/websocket-api/issues/212 + */ + LifeCycle.stop(client); + } + } + + /** + * Since javax.websocket does not have an API for configuring SSL, each implementation + * of javax.websocket has to come up with their own SSL configuration mechanism. + * + * @return the client WebSocketContainer + * @see javax.websocket issue #210 + */ + public static WebSocketContainer getConfiguredWebSocketContainer() throws Exception + { + SslContextFactory ssl = new SslContextFactory.Client(); + ssl.setExcludeCipherSuites(); // echo.websocket.org use WEAK cipher suites + HttpClient httpClient = new HttpClient(ssl); + ClientContainer clientContainer = new ClientContainer(httpClient); + clientContainer.getClient().addManaged(httpClient); // allow clientContainer to own httpClient (for start/stop lifecycle) + clientContainer.start(); + return clientContainer; + } +} diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/examples/SecureWebSocketContainerExample.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/examples/SecureWebSocketContainerExample.java index 50f2c3fecda..b736f10a2e4 100644 --- a/jetty-websocket/javax-websocket-client-impl/src/test/java/examples/SecureWebSocketContainerExample.java +++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/examples/SecureWebSocketContainerExample.java @@ -34,18 +34,11 @@ public class SecureWebSocketContainerExample { public static void main(String[] args) { - String destUri = "wss://echo.websocket.org"; - if (args.length > 0) - { - destUri = args[0]; - } - WebSocketContainer client = null; try { + URI echoUri = URI.create("wss://echo.websocket.org"); client = getConfiguredWebSocketContainer(); - URI echoUri = new URI(destUri); - ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create() .configurator(new OriginServerConfigurator("https://websocket.org")) .build(); From 0f8230c05b425eacc70f95d9d1b9daddf4cd933b Mon Sep 17 00:00:00 2001 From: Jan Bartel Date: Mon, 26 Aug 2019 15:22:20 +1000 Subject: [PATCH 23/55] Issue #3936 Provide write-through modes for the NullSessionCache (#3984) * Issue #3936 Provide write-through modes for the NullSessionCache Signed-off-by: Jan Bartel --- .../etc/sessions/session-cache-null.xml | 5 + .../config/modules/session-cache-null.mod | 1 + .../session/AbstractSessionDataStore.java | 2 +- .../server/session/NullSessionCache.java | 155 ++++++++++ .../session/NullSessionCacheFactory.java | 18 ++ .../server/session/TestSessionDataStore.java | 2 +- .../server/session/NullSessionCacheTest.java | 279 +++++++++++++++++- 7 files changed, 458 insertions(+), 4 deletions(-) diff --git a/jetty-server/src/main/config/etc/sessions/session-cache-null.xml b/jetty-server/src/main/config/etc/sessions/session-cache-null.xml index 1711b172c04..3f030bf05b4 100644 --- a/jetty-server/src/main/config/etc/sessions/session-cache-null.xml +++ b/jetty-server/src/main/config/etc/sessions/session-cache-null.xml @@ -11,6 +11,11 @@ + + + + + diff --git a/jetty-server/src/main/config/modules/session-cache-null.mod b/jetty-server/src/main/config/modules/session-cache-null.mod index abdf2d7e076..6069c8f8168 100644 --- a/jetty-server/src/main/config/modules/session-cache-null.mod +++ b/jetty-server/src/main/config/modules/session-cache-null.mod @@ -18,3 +18,4 @@ etc/sessions/session-cache-null.xml [ini-template] #jetty.session.saveOnCreate=false #jetty.session.removeUnloadableSessions=false +#jetty.session.writeThroughMode=ON_EXIT diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionDataStore.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionDataStore.java index d9078be79f9..9accdb69b16 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionDataStore.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionDataStore.java @@ -126,7 +126,7 @@ public abstract class AbstractSessionDataStore extends ContainerLifeCycle implem LOG.debug("Store: id={}, dirty={}, lsave={}, period={}, elapsed={}", id, data.isDirty(), data.getLastSaved(), savePeriodMs, (System.currentTimeMillis() - lastSave)); //save session if attribute changed or never been saved or time between saves exceeds threshold - if (data.isDirty() || (lastSave <= 0) || ((System.currentTimeMillis() - lastSave) > savePeriodMs)) + if (data.isDirty() || (lastSave <= 0) || ((System.currentTimeMillis() - lastSave) >= savePeriodMs)) { //set the last saved time to now data.setLastSaved(System.currentTimeMillis()); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCache.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCache.java index 5d93456dd24..6e88ab45a7c 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCache.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCache.java @@ -18,7 +18,13 @@ package org.eclipse.jetty.server.session; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSessionAttributeListener; +import javax.servlet.http.HttpSessionBindingEvent; + /** * NullSessionCache @@ -30,6 +36,154 @@ import javax.servlet.http.HttpServletRequest; */ public class NullSessionCache extends AbstractSessionCache { + /** + * If the writethrough mode is ALWAYS or NEW, then use an + * attribute listener to ascertain when the attribute has changed. + * + */ + public class WriteThroughAttributeListener implements HttpSessionAttributeListener + { + Set _sessionsBeingWritten = ConcurrentHashMap.newKeySet(); + + @Override + public void attributeAdded(HttpSessionBindingEvent event) + { + doAttributeChanged(event); + } + + @Override + public void attributeRemoved(HttpSessionBindingEvent event) + { + doAttributeChanged(event); + } + + @Override + public void attributeReplaced(HttpSessionBindingEvent event) + { + doAttributeChanged(event); + } + + private void doAttributeChanged(HttpSessionBindingEvent event) + { + if (_writeThroughMode == WriteThroughMode.ON_EXIT) + return; + + Session session = (Session)event.getSession(); + + SessionDataStore store = getSessionDataStore(); + + if (store == null) + return; + + if (_writeThroughMode == WriteThroughMode.ALWAYS + || (_writeThroughMode == WriteThroughMode.NEW && session.isNew())) + { + //ensure that a call to willPassivate doesn't result in a passivation + //listener removing an attribute, which would cause this listener to + //be called again + if (_sessionsBeingWritten.add(session)) + { + try + { + //should hold the lock on the session, but as sessions are never shared + //with the NullSessionCache, there can be no other thread modifying the + //same session at the same time (although of course there can be another + //request modifying its copy of the session data, so it is impossible + //to guarantee the order of writes). + if (store.isPassivating()) + session.willPassivate(); + store.store(session.getId(), session.getSessionData()); + if (store.isPassivating()) + session.didActivate(); + } + catch (Exception e) + { + LOG.warn("Write through of {} failed", e); + } + finally + { + _sessionsBeingWritten.remove(session); + } + } + } + } + } + + /** + * Defines the circumstances a session will be written to the backing store. + */ + public enum WriteThroughMode + { + /** + * ALWAYS means write through every attribute change. + */ + ALWAYS, + /** + * NEW means to write through every attribute change only + * while the session is freshly created, ie its id has not yet been returned to the client + */ + NEW, + /** + * ON_EXIT means write the session only when the request exits + * (which is the default behaviour of AbstractSessionCache) + */ + ON_EXIT + }; + + private WriteThroughMode _writeThroughMode = WriteThroughMode.ON_EXIT; + protected WriteThroughAttributeListener _listener = null; + + + /** + * @return the writeThroughMode + */ + public WriteThroughMode getWriteThroughMode() + { + return _writeThroughMode; + } + + + /** + * @param writeThroughMode the writeThroughMode to set + */ + public void setWriteThroughMode(WriteThroughMode writeThroughMode) + { + if (getSessionHandler() == null) + throw new IllegalStateException ("No SessionHandler"); + + //assume setting null is the same as ON_EXIT + if (writeThroughMode == null) + { + if (_listener != null) + getSessionHandler().removeEventListener(_listener); + _listener = null; + _writeThroughMode = WriteThroughMode.ON_EXIT; + return; + } + + switch (writeThroughMode) + { + case ON_EXIT: + { + if (_listener != null) + getSessionHandler().removeEventListener(_listener); + _listener = null; + break; + } + case NEW: + case ALWAYS: + { + if (_listener == null) + { + _listener = new WriteThroughAttributeListener(); + getSessionHandler().addEventListener(_listener); + } + break; + } + } + _writeThroughMode = writeThroughMode; + } + /** * @param handler The SessionHandler related to this SessionCache @@ -39,6 +193,7 @@ public class NullSessionCache extends AbstractSessionCache super(handler); super.setEvictionPolicy(EVICT_ON_SESSION_EXIT); } + /** * @see org.eclipse.jetty.server.session.SessionCache#shutdown() diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCacheFactory.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCacheFactory.java index 7d0886148d8..dd4a4cd0986 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCacheFactory.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCacheFactory.java @@ -27,6 +27,23 @@ public class NullSessionCacheFactory implements SessionCacheFactory { boolean _saveOnCreate; boolean _removeUnloadableSessions; + NullSessionCache.WriteThroughMode _writeThroughMode; + + /** + * @return the writeThroughMode + */ + public NullSessionCache.WriteThroughMode getWriteThroughMode() + { + return _writeThroughMode; + } + + /** + * @param writeThroughMode the writeThroughMode to set + */ + public void setWriteThroughMode(NullSessionCache.WriteThroughMode writeThroughMode) + { + _writeThroughMode = writeThroughMode; + } /** * @return the saveOnCreate @@ -69,6 +86,7 @@ public class NullSessionCacheFactory implements SessionCacheFactory NullSessionCache cache = new NullSessionCache(handler); cache.setSaveOnCreate(isSaveOnCreate()); cache.setRemoveUnloadableSessions(isRemoveUnloadableSessions()); + cache.setWriteThroughMode(_writeThroughMode); return cache; } } diff --git a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/TestSessionDataStore.java b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/TestSessionDataStore.java index 805ffdaeb12..98187a396f0 100644 --- a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/TestSessionDataStore.java +++ b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/TestSessionDataStore.java @@ -79,8 +79,8 @@ public class TestSessionDataStore extends AbstractSessionDataStore @Override public void doStore(String id, SessionData data, long lastSaveTime) throws Exception { - _numSaves.addAndGet(1); _map.put(id, data); + _numSaves.addAndGet(1); } @Override diff --git a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/NullSessionCacheTest.java b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/NullSessionCacheTest.java index 6e8aac37e02..3ae38cb6d0a 100644 --- a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/NullSessionCacheTest.java +++ b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/NullSessionCacheTest.java @@ -18,13 +18,21 @@ package org.eclipse.jetty.server.session; +import java.io.Serializable; import java.util.concurrent.TimeUnit; +import javax.servlet.http.HttpSessionActivationListener; +import javax.servlet.http.HttpSessionEvent; + import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletContextHandler; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -32,9 +40,275 @@ import static org.junit.jupiter.api.Assertions.assertTrue; */ public class NullSessionCacheTest { - @Test - public void testEvictOnExit() throws Exception + public static class SerializableTestObject implements Serializable, HttpSessionActivationListener { + int count; + static int passivates = 0; + static int activates = 0; + + public SerializableTestObject(int i) + { + count = i; + } + + @Override + public void sessionWillPassivate(HttpSessionEvent se) + { + //should never be called, as we are replaced with the + //non-serializable object and thus passivate will be called on that + ++passivates; + } + + @Override + public void sessionDidActivate(HttpSessionEvent se) + { + ++activates; + //remove myself, replace with something serializable + se.getSession().setAttribute("pv", new TestObject(count)); + } + } + + + + public static class TestObject implements HttpSessionActivationListener + { + int i; + static int passivates = 0; + static int activates = 0; + + public TestObject(int j) + { + i = j; + } + + @Override + public void sessionWillPassivate(HttpSessionEvent se) + { + ++passivates; + //remove myself, replace with something serializable + se.getSession().setAttribute("pv", new SerializableTestObject(i)); + } + + @Override + public void sessionDidActivate(HttpSessionEvent se) + { + //this should never be called because we replace ourselves during passivation, + //so it is the SerializableTestObject that is activated instead + ++activates; + } + } + + + @Test + public void testWritesWithPassivation() throws Exception + { + //Test that a session that is in the process of being saved cannot cause + //another save via a passivation listener + Server server = new Server(); + + ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); + context.setContextPath("/test"); + context.setServer(server); + + NullSessionCacheFactory cacheFactory = new NullSessionCacheFactory(); + cacheFactory.setWriteThroughMode(NullSessionCache.WriteThroughMode.ALWAYS); + + NullSessionCache cache = (NullSessionCache)cacheFactory.getSessionCache(context.getSessionHandler()); + + TestSessionDataStore store = new TestSessionDataStore(true); //pretend to passivate + cache.setSessionDataStore(store); + context.getSessionHandler().setSessionCache(cache); + + context.start(); + + //make a session + long now = System.currentTimeMillis(); + SessionData data = store.newSessionData("1234", now - 20, now - 10, now - 20, TimeUnit.MINUTES.toMillis(10)); + data.setExpiry(now + TimeUnit.DAYS.toMillis(1)); + Session session = cache.newSession(null, data); //mimic a request making a session + cache.add("1234", session); + //at this point the session should not be saved to the store + assertEquals(0, store._numSaves.get()); + + //set an attribute that is not serializable, should cause a save + TestObject obj = new TestObject(1); + session.setAttribute("pv", obj); + assertTrue(cache._listener._sessionsBeingWritten.isEmpty()); + assertTrue(store.exists("1234")); + assertEquals(1, store._numSaves.get()); + assertEquals(1, TestObject.passivates); + assertEquals(0, TestObject.activates); + assertEquals(1, SerializableTestObject.activates); + assertEquals(0, SerializableTestObject.passivates); + } + + @Test + public void testChangeWriteThroughMode() throws Exception + { + Server server = new Server(); + + ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); + context.setContextPath("/test"); + context.setServer(server); + + NullSessionCacheFactory cacheFactory = new NullSessionCacheFactory(); + + NullSessionCache cache = (NullSessionCache)cacheFactory.getSessionCache(context.getSessionHandler()); + + TestSessionDataStore store = new TestSessionDataStore(); + cache.setSessionDataStore(store); + context.getSessionHandler().setSessionCache(cache); + + assertEquals(NullSessionCache.WriteThroughMode.ON_EXIT, cache.getWriteThroughMode()); + assertNull(cache._listener); + + //change mode to NEW + cache.setWriteThroughMode(NullSessionCache.WriteThroughMode.NEW); + assertEquals(NullSessionCache.WriteThroughMode.NEW, cache.getWriteThroughMode()); + assertNotNull(cache._listener); + assertEquals(1, context.getSessionHandler()._sessionAttributeListeners.size()); + assertTrue(context.getSessionHandler()._sessionAttributeListeners.contains(cache._listener)); + + + //change mode to ALWAYS from NEW, listener should remain + NullSessionCache.WriteThroughAttributeListener old = cache._listener; + cache.setWriteThroughMode(NullSessionCache.WriteThroughMode.ALWAYS); + assertEquals(NullSessionCache.WriteThroughMode.ALWAYS, cache.getWriteThroughMode()); + assertNotNull(cache._listener); + assertSame(old,cache._listener); + assertEquals(1, context.getSessionHandler()._sessionAttributeListeners.size()); + + //check null is same as ON_EXIT + cache.setWriteThroughMode(null); + assertEquals(NullSessionCache.WriteThroughMode.ON_EXIT, cache.getWriteThroughMode()); + assertNull(cache._listener); + assertEquals(0, context.getSessionHandler()._sessionAttributeListeners.size()); + + //change to ON_EXIT + cache.setWriteThroughMode(NullSessionCache.WriteThroughMode.ON_EXIT); + assertEquals(NullSessionCache.WriteThroughMode.ON_EXIT, cache.getWriteThroughMode()); + assertNull(cache._listener); + assertEquals(0, context.getSessionHandler()._sessionAttributeListeners.size()); + } + + + + @Test + public void testWriteThroughAlways() throws Exception + { + Server server = new Server(); + + ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); + context.setContextPath("/test"); + context.setServer(server); + + NullSessionCacheFactory cacheFactory = new NullSessionCacheFactory(); + cacheFactory.setWriteThroughMode(NullSessionCache.WriteThroughMode.ALWAYS); + + NullSessionCache cache = (NullSessionCache)cacheFactory.getSessionCache(context.getSessionHandler()); + + TestSessionDataStore store = new TestSessionDataStore(); + cache.setSessionDataStore(store); + context.getSessionHandler().setSessionCache(cache); + context.start(); + + //make a session + long now = System.currentTimeMillis(); + SessionData data = store.newSessionData("1234", now - 20, now - 10, now - 20, TimeUnit.MINUTES.toMillis(10)); + data.setExpiry(now + TimeUnit.DAYS.toMillis(1)); + Session session = cache.newSession(null, data); //mimic a request making a session + cache.add("1234", session); + //at this point the session should not be saved to the store + assertEquals(0, store._numSaves.get()); + + //check each call to set attribute results in a store + session.setAttribute("colour", "blue"); + assertTrue(store.exists("1234")); + assertEquals(1, store._numSaves.get()); + + //mimic releasing the session after the request is finished + cache.release("1234", session); + assertTrue(store.exists("1234")); + assertFalse(cache.contains("1234")); + assertEquals(2, store._numSaves.get()); + + //simulate a new request using the previously created session + //the session should not now be new + session = cache.get("1234"); //get the session again + session.access(now); //simulate a request + session.setAttribute("spin", "left"); + assertTrue(store.exists("1234")); + assertEquals(3, store._numSaves.get()); + cache.release("1234", session); //finish with the session + + assertFalse(session.isResident()); + } + + @Test + public void testWriteThroughNew () throws Exception + { + Server server = new Server(); + + ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); + context.setContextPath("/test"); + context.setServer(server); + + NullSessionCacheFactory cacheFactory = new NullSessionCacheFactory(); + cacheFactory.setWriteThroughMode(NullSessionCache.WriteThroughMode.NEW); + + NullSessionCache cache = (NullSessionCache)cacheFactory.getSessionCache(context.getSessionHandler()); + + TestSessionDataStore store = new TestSessionDataStore(); + cache.setSessionDataStore(store); + context.getSessionHandler().setSessionCache(cache); + context.start(); + + //make a session + long now = System.currentTimeMillis(); + SessionData data = store.newSessionData("1234", now - 20, now - 10, now - 20, TimeUnit.MINUTES.toMillis(10)); + data.setExpiry(now + TimeUnit.DAYS.toMillis(1)); + Session session = cache.newSession(null, data); //mimic a request making a session + cache.add("1234", session); + //at this point the session should not be saved to the store + assertEquals(0, store._numSaves.get()); + assertTrue(session.isNew()); + + //check each call to set attribute results in a store while the session is new + session.setAttribute("colour", "blue"); + assertTrue(store.exists("1234")); + assertEquals(1, store._numSaves.get()); + session.setAttribute("charge", "positive"); + assertEquals(2, store._numSaves.get()); + + //mimic releasing the session after the request is finished + cache.release("1234", session); + assertTrue(store.exists("1234")); + assertFalse(cache.contains("1234")); + assertEquals(3, store._numSaves.get()); //even if the session isn't dirty, we will save the access time + + + //simulate a new request using the previously created session + //the session should not now be new, so setAttribute should + //not result in a save + session = cache.get("1234"); //get the session again + session.access(now); //simulate a request + assertFalse(session.isNew()); + assertEquals(3, store._numSaves.get()); + session.setAttribute("spin", "left"); + assertTrue(store.exists("1234")); + assertEquals(3, store._numSaves.get()); + session.setAttribute("flavor", "charm"); + assertEquals(3, store._numSaves.get()); + cache.release("1234", session); //finish with the session + assertEquals(4, store._numSaves.get());//release session should write it out + assertFalse(session.isResident()); + } + + + @Test + public void testNotCached() throws Exception + { + //Test the NullSessionCache never contains the session Server server = new Server(); ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); @@ -67,6 +341,7 @@ public class NullSessionCacheTest session = cache.get("1234"); //get the session again session.access(now); //simulate a request cache.release("1234", session); //finish with the session + assertFalse(cache.contains("1234")); assertFalse(session.isResident()); } From bde86467f4e5df595773ab11ed5e80c615b741f3 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Mon, 26 Aug 2019 17:55:58 +1000 Subject: [PATCH 24/55] Issue #3806 - Make Async sendError fully Async (#3912) * Issue #3806 async sendError Avoid using isHandled as a test withing sendError as this can be called asynchronously and is in a race with the normal dispatch of the request, which could also be setting handled status. The ErrorHandler was dispatching directly to a context from within sendError. This meant that an async thread can call sendError and be dispatched to within the servlet container at the same time that the original thread was still dispatched to the container. This commit fixes that problem by using an async dispatch for error pages within the ErrorHandler. However, this introduces a new problem that a well behaved async app will call complete after calling sendError. Thus we have ignore complete ISEs for the remainder of the current async cycle. Fixed the closing of the output after calling sendError. Do not close if the request was async (and thus might be dispatched to an async error) or if it is now async because the error page itself is async. * updates from review * better tests * revert ignore complete * added some TODOs * more TODOs * fixed rename * cleanup ISE and more TODOs * refactored to call sendError for uncaught exceptions rather than onError * more of the refactor * extra tests for sendError from completing state Reworked HttpChannelState and sendError so that sendError is now just a change of state. All the work is done in the ErrorDispatch action, including calling the ErrorHandler. Async not yet working. Additional tests Converted ERRORED state to a separate boolean so it can be used for both Sync and Async dispatches. Removed ASYNC_IO state as it was just the same as DISPATCHED The async onError listener handling is now most likely broken. WIP making sendError simpler and more tests pass WIP handling async and thrown exceptions WIP passing tests Improved thread handling removed bad test Implemented error dispatch on complete properly more fixed tests sendError state looks committed - Added resetContent method to leave more non-content headers during sendError - Fixed security tests - simplified the non dispatch error page writing. Moved towards being able to write async * fixed gzipHandlerTest * Updated handling of timeout errors. According to servlet spec, exceptions thrown from onTimeout should not be passed to onError, but just logged and ignored: If an exception is thrown while invoking methods in an AsyncListener, it is logged and will not affect the invocation of any other AsyncListeners. * This changes several tests. * Dispatcher/ContextHandler changes for new ERROR dispatch handling. Feels a bit fragile! * Fixed tests in jetty-servlets * Fixed tests in jetty-proxy * more test fixes * Fixed head handling reverted unnecessary changes Improved reason handling WIP on fully async error handling. Simplified HttpChannelState state machines to allow for async actions during completing more WIP on fully async error handling. sendError and completion are not both non-blocking, without using a startAsync operation. However we are lacking unit tests that actually exercise those code paths. * Simplified name of states Added test for async completion * Cleanups and javadoc * Cleanups and javadoc * remove snake case * feedback from review * Write error page into fixed pooled buffer Use the response to get/release a pooled buffer into which the error page can be written. Make it a fixed sized buffer and if it overflows then no error page is generated (first overflow turns off showstacks to save space). The ErrorHandler badly needs to be refactored, but we cannot change API in jetty-9 * More test fixes for different error page format * minor cleanups * Cleanup from Review * Fixed javadoc * cleanups and simplifications * Cleanup from Review * renaming and some TODOs * Cleanup from Review * Checkstyle fixes * Cleanup from Review * Code cleanups and simplifications * fixed debug * Cleanup from Review * Ensure response sent before server shutdown * removed unnecessary optimisation * fixed duplicate from merge * Updates from review Signed-off-by: Greg Wilkins --- .../org/eclipse/jetty/http/HttpStatus.java | 14 + .../jetty/io/ByteBufferOutputStream.java | 62 + .../jetty/proxy/AbstractProxyServlet.java | 8 + .../rewrite/handler/ResponsePatternRule.java | 15 +- .../jetty/rewrite/handler/ValidUrlRule.java | 10 +- .../handler/ResponsePatternRuleTest.java | 8 +- .../security/SpecExampleConstraintTest.java | 4 +- .../SpnegoAuthenticatorTest.java | 44 +- .../jetty/server/AsyncContextEvent.java | 2 +- .../org/eclipse/jetty/server/Dispatcher.java | 12 +- .../org/eclipse/jetty/server/HttpChannel.java | 308 ++-- .../jetty/server/HttpChannelState.java | 1350 +++++++++-------- .../eclipse/jetty/server/HttpConnection.java | 14 +- .../org/eclipse/jetty/server/HttpInput.java | 3 +- .../org/eclipse/jetty/server/HttpOutput.java | 265 ++-- .../org/eclipse/jetty/server/Request.java | 10 +- .../org/eclipse/jetty/server/Response.java | 189 +-- .../java/org/eclipse/jetty/server/Server.java | 12 +- .../jetty/server/handler/ContextHandler.java | 31 +- .../jetty/server/handler/ErrorHandler.java | 316 ++-- .../jetty/server/handler/ShutdownHandler.java | 3 +- .../jetty/server/AbstractHttpTest.java | 12 +- .../jetty/server/AsyncCompletionTest.java | 221 +++ .../jetty/server/ErrorHandlerTest.java | 38 - .../jetty/server/HttpInputAsyncStateTest.java | 4 + ...ManyWaysToAsyncCommitBadBehaviourTest.java | 133 -- .../server/HttpManyWaysToAsyncCommitTest.java | 776 ++++++---- .../jetty/server/HttpServerTestBase.java | 106 +- .../jetty/server/HttpServerTestFixture.java | 28 +- .../jetty/server/LocalAsyncContextTest.java | 5 + .../eclipse/jetty/server/ResponseTest.java | 114 +- .../server/handler/NcsaRequestLogTest.java | 25 +- .../handler/SecuredRedirectHandlerTest.java | 1 + .../ssl/SniSslConnectionFactoryTest.java | 69 +- .../jetty/servlet/AsyncContextTest.java | 5 +- .../jetty/servlet/AsyncListenerTest.java | 17 +- .../jetty/servlet/AsyncServletIOTest.java | 8 +- .../jetty/servlet/AsyncServletTest.java | 43 +- .../jetty/servlet/CustomRequestLogTest.java | 3 +- .../eclipse/jetty/servlet/ErrorPageTest.java | 443 +++++- .../jetty/servlet/GzipHandlerTest.java | 5 +- .../org/eclipse/jetty/servlets/DoSFilter.java | 11 +- .../org/eclipse/jetty/util/BufferUtil.java | 13 +- .../jetty/util/thread/QueuedThreadPool.java | 3 +- .../tests/WebSocketConnectionStatsTest.java | 2 +- .../tests/WebSocketNegotiationTest.java | 2 +- .../server/WebSocketInvalidVersionTest.java | 2 +- .../jetty/tests/distribution/BadAppTests.java | 8 +- 48 files changed, 2893 insertions(+), 1884 deletions(-) create mode 100644 jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferOutputStream.java create mode 100644 jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java delete mode 100644 jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitBadBehaviourTest.java diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java index 887bb636259..20e6f8cf431 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java @@ -320,6 +320,20 @@ public class HttpStatus } } + public static boolean hasNoBody(int status) + { + switch (status) + { + case NO_CONTENT_204: + case NOT_MODIFIED_304: + case PARTIAL_CONTENT_206: + return true; + + default: + return status < OK_200; + } + } + /** * Simple test against an code to determine if it falls into the * Informational message category as defined in the = 400) { - response.sendError(code, _reason); + if (!StringUtil.isBlank(_reason)) + { + // use both setStatusWithReason (to set the reason) and sendError to set the message + Request.getBaseRequest(request).getResponse().setStatusWithReason(code, _reason); + response.sendError(code, _reason); + } + else + { + response.sendError(code); + } } else { - response.setStatus(code); + response.setStatus(code, _reason); } return target; } diff --git a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ValidUrlRule.java b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ValidUrlRule.java index 2ac00d233d2..1e3207bb7dc 100644 --- a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ValidUrlRule.java +++ b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ValidUrlRule.java @@ -22,6 +22,8 @@ import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -88,7 +90,13 @@ public class ValidUrlRule extends Rule // status code 400 and up are error codes so include a reason if (code >= 400) { - response.sendError(code, _reason); + if (StringUtil.isBlank(_reason)) + response.sendError(code); + else + { + Request.getBaseRequest(request).getResponse().setStatusWithReason(code, _reason); + response.sendError(code, _reason); + } } else { diff --git a/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRuleTest.java b/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRuleTest.java index aeca52412ed..fd6c1290b9b 100644 --- a/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRuleTest.java +++ b/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRuleTest.java @@ -20,6 +20,8 @@ package org.eclipse.jetty.rewrite.handler; import java.io.IOException; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.server.Dispatcher; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -59,7 +61,7 @@ public class ResponsePatternRuleTest extends AbstractRuleTestCase _rule.apply(null, _request, _response); assertEquals(i, _response.getStatus()); - assertEquals(null, _response.getReason()); + assertEquals("reason" + i, _response.getReason()); } } @@ -72,7 +74,7 @@ public class ResponsePatternRuleTest extends AbstractRuleTestCase _rule.apply(null, _request, _response); assertEquals(i, _response.getStatus()); - assertEquals("", _response.getReason()); + assertEquals(HttpStatus.getMessage(i), _request.getAttribute(Dispatcher.ERROR_MESSAGE)); super.reset(); } } @@ -87,7 +89,7 @@ public class ResponsePatternRuleTest extends AbstractRuleTestCase _rule.apply(null, _request, _response); assertEquals(i, _response.getStatus()); - assertEquals("reason-" + i, _response.getReason()); + assertEquals("reason-" + i, _request.getAttribute(Dispatcher.ERROR_MESSAGE)); super.reset(); } } diff --git a/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java b/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java index 2f8d243071c..cd0d46a7a98 100644 --- a/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java +++ b/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java @@ -44,6 +44,7 @@ import org.junit.jupiter.api.Test; import static java.nio.charset.StandardCharsets.ISO_8859_1; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -321,7 +322,8 @@ public class SpecExampleConstraintTest response = _connector.getResponse("POST /ctx/acme/wholesale/index.html HTTP/1.0\r\n" + "Authorization: Basic " + encodedChris + "\r\n" + "\r\n"); - assertThat(response, startsWith("HTTP/1.1 403 !")); + assertThat(response, startsWith("HTTP/1.1 403 Forbidden")); + assertThat(response, containsString("!Secure")); //a user in role HOMEOWNER can do a GET response = _connector.getResponse("GET /ctx/acme/retail/index.html HTTP/1.0\r\n" + diff --git a/jetty-security/src/test/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticatorTest.java b/jetty-security/src/test/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticatorTest.java index 3e771934fa9..00af87e7ecc 100644 --- a/jetty-security/src/test/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticatorTest.java +++ b/jetty-security/src/test/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticatorTest.java @@ -18,6 +18,7 @@ package org.eclipse.jetty.security.authentication; +import java.io.IOException; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.HttpFields; @@ -26,6 +27,7 @@ import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.server.Authentication; import org.eclipse.jetty.server.HttpChannel; +import org.eclipse.jetty.server.HttpChannelState; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpOutput; import org.eclipse.jetty.server.Request; @@ -34,6 +36,8 @@ import org.eclipse.jetty.server.Server; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +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.assertTrue; @@ -61,21 +65,27 @@ public class SpnegoAuthenticatorTest { return null; } - }; - Request req = new Request(channel, null); - HttpOutput out = new HttpOutput(channel) - { + @Override - public void close() + protected HttpOutput newHttpOutput() { - return; + return new HttpOutput(this) + { + @Override + public void close() {} + + @Override + public void flush() throws IOException {} + }; } }; - Response res = new Response(channel, out); + Request req = channel.getRequest(); + Response res = channel.getResponse(); MetaData.Request metadata = new MetaData.Request(new HttpFields()); metadata.setURI(new HttpURI("http://localhost")); req.setMetaData(metadata); + assertThat(channel.getState().handling(), is(HttpChannelState.Action.DISPATCH)); assertEquals(Authentication.SEND_CONTINUE, _authenticator.validateRequest(req, res, true)); assertEquals(HttpHeader.NEGOTIATE.asString(), res.getHeader(HttpHeader.WWW_AUTHENTICATE.asString())); assertEquals(HttpServletResponse.SC_UNAUTHORIZED, res.getStatus()); @@ -91,17 +101,22 @@ public class SpnegoAuthenticatorTest { return null; } - }; - Request req = new Request(channel, null); - HttpOutput out = new HttpOutput(channel) - { + @Override - public void close() + protected HttpOutput newHttpOutput() { - return; + return new HttpOutput(this) + { + @Override + public void close() {} + + @Override + public void flush() throws IOException {} + }; } }; - Response res = new Response(channel, out); + Request req = channel.getRequest(); + Response res = channel.getResponse(); HttpFields http_fields = new HttpFields(); // Create a bogus Authorization header. We don't care about the actual credentials. http_fields.add(HttpHeader.AUTHORIZATION, "Basic asdf"); @@ -109,6 +124,7 @@ public class SpnegoAuthenticatorTest metadata.setURI(new HttpURI("http://localhost")); req.setMetaData(metadata); + assertThat(channel.getState().handling(), is(HttpChannelState.Action.DISPATCH)); assertEquals(Authentication.SEND_CONTINUE, _authenticator.validateRequest(req, res, true)); assertEquals(HttpHeader.NEGOTIATE.asString(), res.getHeader(HttpHeader.WWW_AUTHENTICATE.asString())); assertEquals(HttpServletResponse.SC_UNAUTHORIZED, res.getStatus()); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java index 0cd93ff67be..52520ffd71f 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java @@ -160,7 +160,7 @@ public class AsyncContextEvent extends AsyncEvent implements Runnable Scheduler.Task task = _timeoutTask; _timeoutTask = null; if (task != null) - _state.getHttpChannel().execute(() -> _state.onTimeout()); + _state.timeout(); } public void addThrowable(Throwable e) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java index 840335af2af..09249f955ab 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java @@ -42,8 +42,6 @@ public class Dispatcher implements RequestDispatcher { private static final Logger LOG = Log.getLogger(Dispatcher.class); - public static final String __ERROR_DISPATCH = "org.eclipse.jetty.server.Dispatcher.ERROR"; - /** * Dispatch include attribute names */ @@ -83,15 +81,7 @@ public class Dispatcher implements RequestDispatcher public void error(ServletRequest request, ServletResponse response) throws ServletException, IOException { - try - { - request.setAttribute(__ERROR_DISPATCH, Boolean.TRUE); - forward(request, response, DispatcherType.ERROR); - } - finally - { - request.setAttribute(__ERROR_DISPATCH, null); - } + forward(request, response, DispatcherType.ERROR); } @Override diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index bd2ad60a63b..31714973255 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -25,7 +25,6 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -33,12 +32,12 @@ import java.util.function.Function; import java.util.function.Supplier; import javax.servlet.DispatcherType; import javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpGenerator; import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpVersion; @@ -51,6 +50,7 @@ import org.eclipse.jetty.io.QuietException; import org.eclipse.jetty.server.HttpChannelState.Action; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ErrorHandler; +import org.eclipse.jetty.server.handler.ErrorHandler.ErrorPageMapper; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.SharedBlockingCallback.Blocker; @@ -71,8 +71,6 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor { private static final Logger LOG = Log.getLogger(HttpChannel.class); - private final AtomicBoolean _committed = new AtomicBoolean(); - private final AtomicBoolean _responseCompleted = new AtomicBoolean(); private final AtomicLong _requests = new AtomicLong(); private final Connector _connector; private final Executor _executor; @@ -121,6 +119,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor _state); } + public boolean isSendError() + { + return _state.isSendError(); + } + protected HttpInput newHttpInput(HttpChannelState state) { return new HttpInput(state); @@ -284,8 +287,6 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor public void recycle() { - _committed.set(false); - _responseCompleted.set(false); _request.recycle(); _response.recycle(); _committedMetaData = null; @@ -320,7 +321,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor public boolean handle() { if (LOG.isDebugEnabled()) - LOG.debug("{} handle {} ", this, _request.getHttpURI()); + LOG.debug("handle {} {} ", _request.getHttpURI(), this); HttpChannelState.Action action = _state.handling(); @@ -334,19 +335,18 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor try { if (LOG.isDebugEnabled()) - LOG.debug("{} action {}", this, action); + LOG.debug("action {} {}", action, this); switch (action) { case TERMINATED: + onCompleted(); + break loop; + case WAIT: // break loop without calling unhandle break loop; - case NOOP: - // do nothing other than call unhandle - break; - case DISPATCH: { if (!_request.hasMetaData()) @@ -354,35 +354,17 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor _request.setHandled(false); _response.getHttpOutput().reopen(); - try + dispatch(DispatcherType.REQUEST, () -> { - _request.setDispatcherType(DispatcherType.REQUEST); - notifyBeforeDispatch(_request); - - List customizers = _configuration.getCustomizers(); - if (!customizers.isEmpty()) + for (HttpConfiguration.Customizer customizer : _configuration.getCustomizers()) { - for (HttpConfiguration.Customizer customizer : customizers) - { - customizer.customize(getConnector(), _configuration, _request); - if (_request.isHandled()) - break; - } + customizer.customize(getConnector(), _configuration, _request); + if (_request.isHandled()) + return; } + getServer().handle(HttpChannel.this); + }); - if (!_request.isHandled()) - getServer().handle(this); - } - catch (Throwable x) - { - notifyDispatchFailure(_request, x); - throw x; - } - finally - { - notifyAfterDispatch(_request); - _request.setDispatcherType(null); - } break; } @@ -391,70 +373,70 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor _request.setHandled(false); _response.getHttpOutput().reopen(); - try - { - _request.setDispatcherType(DispatcherType.ASYNC); - notifyBeforeDispatch(_request); - getServer().handleAsync(this); - } - catch (Throwable x) - { - notifyDispatchFailure(_request, x); - throw x; - } - finally - { - notifyAfterDispatch(_request); - _request.setDispatcherType(null); - } + dispatch(DispatcherType.ASYNC,() -> getServer().handleAsync(this)); break; } - case ERROR_DISPATCH: + case ASYNC_TIMEOUT: + _state.onTimeout(); + break; + + case SEND_ERROR: { try { - _response.reset(true); - Integer icode = (Integer)_request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); - int code = icode != null ? icode : HttpStatus.INTERNAL_SERVER_ERROR_500; - _response.setStatus(code); - _request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, code); + // Get ready to send an error response _request.setHandled(false); + _response.resetContent(); _response.getHttpOutput().reopen(); - try + // the following is needed as you cannot trust the response code and reason + // as those could have been modified after calling sendError + Integer code = (Integer)_request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); + _response.setStatus(code != null ? code : HttpStatus.INTERNAL_SERVER_ERROR_500); + + ContextHandler.Context context = (ContextHandler.Context)_request.getAttribute(ErrorHandler.ERROR_CONTEXT); + ErrorHandler errorHandler = ErrorHandler.getErrorHandler(getServer(), context == null ? null : context.getContextHandler()); + + // If we can't have a body, then create a minimal error response. + if (HttpStatus.hasNoBody(_response.getStatus()) || errorHandler == null || !errorHandler.errorPageForMethod(_request.getMethod())) { - _request.setDispatcherType(DispatcherType.ERROR); - notifyBeforeDispatch(_request); - getServer().handle(this); + sendResponseAndComplete(); + break; } - catch (Throwable x) + + // Look for an error page dispatcher + String errorPage = (errorHandler instanceof ErrorPageMapper) ? ((ErrorPageMapper)errorHandler).getErrorPage(_request) : null; + Dispatcher errorDispatcher = errorPage != null ? (Dispatcher)context.getRequestDispatcher(errorPage) : null; + if (errorDispatcher == null) { - notifyDispatchFailure(_request, x); - throw x; + // Allow ErrorHandler to generate response + errorHandler.handle(null, _request, _request, _response); + _request.setHandled(true); } - finally + else { - notifyAfterDispatch(_request); - _request.setDispatcherType(null); + // Do the error page dispatch + dispatch(DispatcherType.ERROR,() -> errorDispatcher.error(_request, _response)); } } catch (Throwable x) { if (LOG.isDebugEnabled()) LOG.debug("Could not perform ERROR dispatch, aborting", x); - Throwable failure = (Throwable)_request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); - if (failure == null) - { - minimalErrorResponse(x); - } + if (_state.isResponseCommitted()) + abort(x); else { - if (x != failure) - failure.addSuppressed(x); - minimalErrorResponse(failure); + _response.resetContent(); + sendResponseAndComplete(); } } + finally + { + // clean up the context that was set in Response.sendError + _request.removeAttribute(ErrorHandler.ERROR_CONTEXT); + } break; } @@ -463,6 +445,12 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor throw _state.getAsyncContextEvent().getThrowable(); } + case READ_REGISTER: + { + onAsyncWaitForContent(); + break; + } + case READ_PRODUCE: { _request.getHttpInput().asyncReadProduce(); @@ -491,45 +479,37 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor case COMPLETE: { - try + if (!_response.isCommitted() && !_request.isHandled() && !_response.getHttpOutput().isClosed()) { - if (!_response.isCommitted() && !_request.isHandled()) - { - _response.sendError(HttpStatus.NOT_FOUND_404); - } - else - { - // RFC 7230, section 3.3. - int status = _response.getStatus(); - boolean hasContent = !(_request.isHead() || - HttpMethod.CONNECT.is(_request.getMethod()) && status == HttpStatus.OK_200 || - HttpStatus.isInformational(status) || - status == HttpStatus.NO_CONTENT_204 || - status == HttpStatus.NOT_MODIFIED_304); - if (hasContent && !_response.isContentComplete(_response.getHttpOutput().getWritten())) - { - if (isCommitted()) - abort(new IOException("insufficient content written")); - else - _response.sendError(HttpStatus.INTERNAL_SERVER_ERROR_500, "insufficient content written"); - } - } - _response.closeOutput(); - } - finally - { - _request.setHandled(true); - _state.onComplete(); - onCompleted(); + _response.sendError(HttpStatus.NOT_FOUND_404); + break; } - break loop; + // RFC 7230, section 3.3. + if (!_request.isHead() && !_response.isContentComplete(_response.getHttpOutput().getWritten())) + { + if (isCommitted()) + abort(new IOException("insufficient content written")); + else + { + _response.sendError(HttpStatus.INTERNAL_SERVER_ERROR_500, "insufficient content written"); + break; + } + } + + // TODO Currently a blocking/aborting consumeAll is done in the handling of the TERMINATED + // TODO Action triggered by the completed callback below. It would be possible to modify the + // TODO callback to do a non-blocking consumeAll at this point and only call completed when + // TODO that is done. + + // Set a close callback on the HttpOutput to make it an async callback + _response.closeOutput(Callback.from(_state::completed)); + + break; } default: - { - throw new IllegalStateException("state=" + _state); - } + throw new IllegalStateException(this.toString()); } } catch (Throwable failure) @@ -544,26 +524,29 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor } if (LOG.isDebugEnabled()) - LOG.debug("{} handle exit, result {}", this, action); + LOG.debug("!handle {} {}", action, this); boolean suspended = action == Action.WAIT; return !suspended; } - protected void sendError(int code, String reason) + private void dispatch(DispatcherType type, Dispatchable dispatchable) throws IOException, ServletException { try { - _response.sendError(code, reason); + _request.setDispatcherType(type); + notifyBeforeDispatch(_request); + dispatchable.dispatch(); } catch (Throwable x) { - if (LOG.isDebugEnabled()) - LOG.debug("Could not send error " + code + " " + reason, x); + notifyDispatchFailure(_request, x); + throw x; } finally { - _state.errorComplete(); + notifyAfterDispatch(_request); + _request.setDispatcherType(null); } } @@ -591,27 +574,19 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor { // No stack trace unless there is debug turned on if (LOG.isDebugEnabled()) - LOG.debug(_request.getRequestURI(), failure); + LOG.warn("handleException " + _request.getRequestURI(), failure); else - LOG.warn("{} {}", _request.getRequestURI(), noStack.toString()); + LOG.warn("handleException {} {}", _request.getRequestURI(), noStack.toString()); } else { LOG.warn(_request.getRequestURI(), failure); } - try - { + if (isCommitted()) + abort(failure); + else _state.onError(failure); - } - catch (Throwable e) - { - if (e != failure) - failure.addSuppressed(e); - LOG.warn("ERROR dispatch failed", failure); - // Try to send a minimal response. - minimalErrorResponse(failure); - } } /** @@ -635,30 +610,17 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor return null; } - private void minimalErrorResponse(Throwable failure) + public void sendResponseAndComplete() { try { - int code = 500; - Integer status = (Integer)_request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); - if (status != null) - code = status.intValue(); - else - { - Throwable cause = unwrap(failure, BadMessageException.class); - if (cause instanceof BadMessageException) - code = ((BadMessageException)cause).getCode(); - } - - _response.reset(true); - _response.setStatus(code); - _response.flushBuffer(); + _request.setHandled(true); + _state.completing(); + sendResponse(null, _response.getHttpOutput().getBuffer(), true, Callback.from(_state::completed)); } catch (Throwable x) { - if (x != failure) - failure.addSuppressed(x); - abort(failure); + abort(x); } } @@ -676,11 +638,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor public String toString() { long timeStamp = _request.getTimeStamp(); - return String.format("%s@%x{r=%s,c=%b,c=%b/%b,a=%s,uri=%s,age=%d}", + return String.format("%s@%x{s=%s,r=%s,c=%b/%b,a=%s,uri=%s,age=%d}", getClass().getSimpleName(), hashCode(), + _state, _requests, - _committed.get(), isRequestCompleted(), isResponseCompleted(), _state.getState(), @@ -717,7 +679,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor public boolean onContent(HttpInput.Content content) { if (LOG.isDebugEnabled()) - LOG.debug("{} onContent {}", this, content); + LOG.debug("onContent {} {}", this, content); notifyRequestContent(_request, content.getByteBuffer()); return _request.getHttpInput().addContent(content); } @@ -725,7 +687,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor public boolean onContentComplete() { if (LOG.isDebugEnabled()) - LOG.debug("{} onContentComplete", this); + LOG.debug("onContentComplete {}", this); notifyRequestContentEnd(_request); return false; } @@ -733,7 +695,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor public void onTrailers(HttpFields trailers) { if (LOG.isDebugEnabled()) - LOG.debug("{} onTrailers {}", this, trailers); + LOG.debug("onTrailers {} {}", this, trailers); _trailers = trailers; notifyRequestTrailers(_request); } @@ -741,7 +703,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor public boolean onRequestComplete() { if (LOG.isDebugEnabled()) - LOG.debug("{} onRequestComplete", this); + LOG.debug("onRequestComplete {}", this); boolean result = _request.getHttpInput().eof(); notifyRequestEnd(_request); return result; @@ -750,7 +712,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor public void onCompleted() { if (LOG.isDebugEnabled()) - LOG.debug("COMPLETE for {} written={}", getRequest().getRequestURI(), getBytesWritten()); + LOG.debug("onCompleted for {} written={}", getRequest().getRequestURI(), getBytesWritten()); if (_requestLog != null) _requestLog.log(_request, _response); @@ -773,7 +735,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor { int status = failure.getCode(); String reason = failure.getReason(); - if (status < 400 || status > 599) + if (status < HttpStatus.BAD_REQUEST_400 || status > 599) failure = new BadMessageException(HttpStatus.BAD_REQUEST_400, reason, failure); notifyRequestFailure(_request, failure); @@ -823,9 +785,9 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor } } - protected boolean sendResponse(MetaData.Response info, ByteBuffer content, boolean complete, final Callback callback) + public boolean sendResponse(MetaData.Response info, ByteBuffer content, boolean complete, final Callback callback) { - boolean committing = _committed.compareAndSet(false, true); + boolean committing = _state.commitResponse(); if (LOG.isDebugEnabled()) LOG.debug("sendResponse info={} content={} complete={} committing={} callback={}", @@ -844,7 +806,9 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor // wrap callback to process 100 responses final int status = info.getStatus(); - final Callback committed = (status < 200 && status >= 100) ? new Send100Callback(callback) : new SendCallback(callback, content, true, complete); + final Callback committed = (status < HttpStatus.OK_200 && status >= HttpStatus.CONTINUE_100) + ? new Send100Callback(callback) + : new SendCallback(callback, content, true, complete); notifyResponseBegin(_request); @@ -891,7 +855,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor public boolean isCommitted() { - return _committed.get(); + return _state.isResponseCommitted(); } /** @@ -907,7 +871,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor */ public boolean isResponseCompleted() { - return _responseCompleted.get(); + return _state.isResponseCompleted(); } public boolean isPersistent() @@ -970,8 +934,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor */ public void abort(Throwable failure) { - notifyResponseFailure(_request, failure); - _transport.abort(failure); + if (_state.abortResponse()) + { + notifyResponseFailure(_request, failure); + _transport.abort(failure); + } } private void notifyRequestBegin(Request request) @@ -1095,6 +1062,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor } } + interface Dispatchable + { + void dispatch() throws IOException, ServletException; + } + /** *

Listener for {@link HttpChannel} events.

*

HttpChannel will emit events for the various phases it goes through while @@ -1280,16 +1252,15 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor public void succeeded() { _written += _length; + if (_complete) + _response.getHttpOutput().closed(); super.succeeded(); if (_commit) notifyResponseCommit(_request); if (_length > 0) notifyResponseContent(_request, _content); - if (_complete) - { - _responseCompleted.set(true); + if (_complete && _state.completeResponse()) notifyResponseEnd(_request); - } } @Override @@ -1305,13 +1276,14 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor @Override public void succeeded() { - super.failed(x); _response.getHttpOutput().closed(); + super.failed(x); } @Override public void failed(Throwable th) { + _response.getHttpOutput().closed(); abort(x); super.failed(x); } @@ -1335,7 +1307,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor @Override public void succeeded() { - if (_committed.compareAndSet(true, false)) + if (_state.partialResponse()) super.succeeded(); else super.failed(new IllegalStateException()); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index 0b8029ab10f..d75eb7a7e5b 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -21,9 +21,7 @@ package org.eclipse.jetty.server; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; import javax.servlet.AsyncListener; -import javax.servlet.RequestDispatcher; import javax.servlet.ServletContext; import javax.servlet.ServletResponse; import javax.servlet.UnavailableException; @@ -32,14 +30,16 @@ import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandler.Context; +import org.eclipse.jetty.server.handler.ErrorHandler; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; -import org.eclipse.jetty.util.thread.Locker; import org.eclipse.jetty.util.thread.Scheduler; import static javax.servlet.RequestDispatcher.ERROR_EXCEPTION; import static javax.servlet.RequestDispatcher.ERROR_EXCEPTION_TYPE; import static javax.servlet.RequestDispatcher.ERROR_MESSAGE; +import static javax.servlet.RequestDispatcher.ERROR_REQUEST_URI; +import static javax.servlet.RequestDispatcher.ERROR_SERVLET_NAME; import static javax.servlet.RequestDispatcher.ERROR_STATUS_CODE; /** @@ -51,21 +51,77 @@ public class HttpChannelState private static final long DEFAULT_TIMEOUT = Long.getLong("org.eclipse.jetty.server.HttpChannelState.DEFAULT_TIMEOUT", 30000L); - /** + /* * The state of the HttpChannel,used to control the overall lifecycle. + *

+     *     IDLE <-----> HANDLING ----> WAITING
+     *       |                 ^       /
+     *       |                  \     /
+     *       v                   \   v
+     *    UPGRADED               WOKEN
+     * 
*/ public enum State { - IDLE, // Idle request - DISPATCHED, // Request dispatched to filter/servlet - THROWN, // Exception thrown while DISPATCHED - ASYNC_WAIT, // Suspended and waiting - ASYNC_WOKEN, // Dispatch to handle from ASYNC_WAIT - ASYNC_IO, // Dispatched for async IO - ASYNC_ERROR, // Async error from ASYNC_WAIT - COMPLETING, // Response is completable - COMPLETED, // Response is completed - UPGRADED // Request upgraded the connection + IDLE, // Idle request + HANDLING, // Request dispatched to filter/servlet or Async IO callback + WAITING, // Suspended and waiting + WOKEN, // Dispatch to handle from ASYNC_WAIT + UPGRADED // Request upgraded the connection + } + + /* + * The state of the request processing lifecycle. + *
+     *       BLOCKING <----> COMPLETING ---> COMPLETED
+     *       ^  |  ^            ^
+     *      /   |   \           |
+     *     |    |    DISPATCH   |
+     *     |    |    ^  ^       |
+     *     |    v   /   |       |
+     *     |  ASYNC -------> COMPLETE
+     *     |    |       |       ^
+     *     |    v       |       |
+     *     |  EXPIRE    |       |
+     *      \   |      /        |
+     *       \  v     /         |
+     *       EXPIRING ----------+
+     * 
+ */ + private enum RequestState + { + BLOCKING, // Blocking request dispatched + ASYNC, // AsyncContext.startAsync() has been called + DISPATCH, // AsyncContext.dispatch() has been called + EXPIRE, // AsyncContext timeout has happened + EXPIRING, // AsyncListeners are being called + COMPLETE, // AsyncContext.complete() has been called + COMPLETING, // Request is being closed (maybe asynchronously) + COMPLETED // Response is completed + } + + /* + * The input readiness state, which works together with {@link HttpInput.State} + */ + private enum InputState + { + IDLE, // No isReady; No data + REGISTER, // isReady()==false handling; No data + REGISTERED, // isReady()==false !handling; No data + POSSIBLE, // isReady()==false async read callback called (http/1 only) + PRODUCING, // isReady()==false READ_PRODUCE action is being handled (http/1 only) + READY // isReady() was false, onContentAdded has been called + } + + /* + * The output committed state, which works together with {@link HttpOutput.State} + */ + private enum OutputState + { + OPEN, + COMMITTED, + COMPLETED, + ABORTED, } /** @@ -73,51 +129,28 @@ public class HttpChannelState */ public enum Action { - NOOP, // No action DISPATCH, // handle a normal request dispatch ASYNC_DISPATCH, // handle an async request dispatch - ERROR_DISPATCH, // handle a normal error + SEND_ERROR, // Generate an error page or error dispatch ASYNC_ERROR, // handle an async error + ASYNC_TIMEOUT, // call asyncContext onTimeout WRITE_CALLBACK, // handle an IO write callback + READ_REGISTER, // Register for fill interest READ_PRODUCE, // Check is a read is possible by parsing/filling READ_CALLBACK, // handle an IO read callback - COMPLETE, // Complete the response + COMPLETE, // Complete the response by closing output TERMINATED, // No further actions WAIT, // Wait for further events } - /** - * The state of the servlet async API. - */ - private enum Async - { - NOT_ASYNC, - STARTED, // AsyncContext.startAsync() has been called - DISPATCH, // AsyncContext.dispatch() has been called - COMPLETE, // AsyncContext.complete() has been called - EXPIRING, // AsyncContext timeout just happened - EXPIRED, // AsyncContext timeout has been processed - ERRORING, // An error just happened - ERRORED // The error has been processed - } - - private enum AsyncRead - { - IDLE, // No isReady; No data - REGISTER, // isReady()==false handling; No data - REGISTERED, // isReady()==false !handling; No data - POSSIBLE, // isReady()==false async read callback called (http/1 only) - PRODUCING, // isReady()==false READ_PRODUCE action is being handled (http/1 only) - READY // isReady() was false, onContentAdded has been called - } - - private final Locker _locker = new Locker(); private final HttpChannel _channel; private List _asyncListeners; - private State _state; - private Async _async; - private boolean _initial; - private AsyncRead _asyncRead = AsyncRead.IDLE; + private State _state = State.IDLE; + private RequestState _requestState = RequestState.BLOCKING; + private OutputState _outputState = OutputState.OPEN; + private InputState _inputState = InputState.IDLE; + private boolean _initial = true; + private boolean _sendError; private boolean _asyncWritePossible; private long _timeoutMs = DEFAULT_TIMEOUT; private AsyncContextEvent _event; @@ -125,14 +158,11 @@ public class HttpChannelState protected HttpChannelState(HttpChannel channel) { _channel = channel; - _state = State.IDLE; - _async = Async.NOT_ASYNC; - _initial = true; } public State getState() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return _state; } @@ -140,7 +170,7 @@ public class HttpChannelState public void addListener(AsyncListener listener) { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (_asyncListeners == null) _asyncListeners = new ArrayList<>(); @@ -150,7 +180,7 @@ public class HttpChannelState public boolean hasListener(AsyncListener listener) { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (_asyncListeners == null) return false; @@ -167,9 +197,17 @@ public class HttpChannelState } } + public boolean isSendError() + { + synchronized (this) + { + return _sendError; + } + } + public void setTimeout(long ms) { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { _timeoutMs = ms; } @@ -177,7 +215,7 @@ public class HttpChannelState public long getTimeout() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return _timeoutMs; } @@ -185,7 +223,7 @@ public class HttpChannelState public AsyncContextEvent getAsyncContextEvent() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return _event; } @@ -194,43 +232,139 @@ public class HttpChannelState @Override public String toString() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return toStringLocked(); } } - public String toStringLocked() + private String toStringLocked() { - return String.format("%s@%x{s=%s a=%s i=%b r=%s w=%b}", + return String.format("%s@%x{%s}", getClass().getSimpleName(), hashCode(), - _state, - _async, - _initial, - _asyncRead, - _asyncWritePossible); + getStatusStringLocked()); } private String getStatusStringLocked() { - return String.format("s=%s i=%b a=%s", _state, _initial, _async); + return String.format("s=%s rs=%s os=%s is=%s awp=%b se=%b i=%b al=%d", + _state, + _requestState, + _outputState, + _inputState, + _asyncWritePossible, + _sendError, + _initial, + _asyncListeners == null ? 0 : _asyncListeners.size()); } public String getStatusString() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return getStatusStringLocked(); } } + public boolean commitResponse() + { + synchronized (this) + { + switch (_outputState) + { + case OPEN: + _outputState = OutputState.COMMITTED; + return true; + + default: + return false; + } + } + } + + public boolean partialResponse() + { + synchronized (this) + { + switch (_outputState) + { + case COMMITTED: + _outputState = OutputState.OPEN; + return true; + + default: + return false; + } + } + } + + public boolean completeResponse() + { + synchronized (this) + { + switch (_outputState) + { + case OPEN: + case COMMITTED: + _outputState = OutputState.COMPLETED; + return true; + + default: + return false; + } + } + } + + public boolean isResponseCommitted() + { + synchronized (this) + { + switch (_outputState) + { + case OPEN: + return false; + default: + return true; + } + } + } + + public boolean isResponseCompleted() + { + synchronized (this) + { + return _outputState == OutputState.COMPLETED; + } + } + + public boolean abortResponse() + { + synchronized (this) + { + switch (_outputState) + { + case ABORTED: + return false; + + case OPEN: + _channel.getResponse().setStatus(500); + _outputState = OutputState.ABORTED; + return true; + + default: + _outputState = OutputState.ABORTED; + return true; + } + } + } + /** * @return Next handling of the request should proceed */ - protected Action handling() + public Action handling() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("handling {}", toStringLocked()); @@ -238,90 +372,168 @@ public class HttpChannelState switch (_state) { case IDLE: + if (_requestState != RequestState.BLOCKING) + throw new IllegalStateException(getStatusStringLocked()); _initial = true; - _state = State.DISPATCHED; + _state = State.HANDLING; return Action.DISPATCH; - case COMPLETING: - case COMPLETED: - return Action.TERMINATED; - - case ASYNC_WOKEN: - switch (_asyncRead) + case WOKEN: + if (_event != null && _event.getThrowable() != null && !_sendError) { - case POSSIBLE: - _state = State.ASYNC_IO; - _asyncRead = AsyncRead.PRODUCING; - return Action.READ_PRODUCE; - case READY: - _state = State.ASYNC_IO; - _asyncRead = AsyncRead.IDLE; - return Action.READ_CALLBACK; - case REGISTER: - case PRODUCING: - case IDLE: - case REGISTERED: - break; - default: - throw new IllegalStateException(getStatusStringLocked()); + _state = State.HANDLING; + return Action.ASYNC_ERROR; } - if (_asyncWritePossible) - { - _state = State.ASYNC_IO; - _asyncWritePossible = false; - return Action.WRITE_CALLBACK; - } + Action action = nextAction(true); + if (LOG.isDebugEnabled()) + LOG.debug("nextAction(true) {} {}", action, toStringLocked()); + return action; - switch (_async) - { - case COMPLETE: - _state = State.COMPLETING; - return Action.COMPLETE; - case DISPATCH: - _state = State.DISPATCHED; - _async = Async.NOT_ASYNC; - return Action.ASYNC_DISPATCH; - case EXPIRED: - case ERRORED: - _state = State.DISPATCHED; - _async = Async.NOT_ASYNC; - return Action.ERROR_DISPATCH; - case STARTED: - case EXPIRING: - case ERRORING: - _state = State.ASYNC_WAIT; - return Action.NOOP; - case NOT_ASYNC: - default: - throw new IllegalStateException(getStatusStringLocked()); - } - - case ASYNC_ERROR: - return Action.ASYNC_ERROR; - - case ASYNC_IO: - case ASYNC_WAIT: - case DISPATCHED: - case UPGRADED: default: throw new IllegalStateException(getStatusStringLocked()); } } } + /** + * Signal that the HttpConnection has finished handling the request. + * For blocking connectors, this call may block if the request has + * been suspended (startAsync called). + * + * @return next actions + * be handled again (eg because of a resume that happened before unhandle was called) + */ + protected Action unhandle() + { + boolean readInterested = false; + + synchronized (this) + { + if (LOG.isDebugEnabled()) + LOG.debug("unhandle {}", toStringLocked()); + + if (_state != State.HANDLING) + throw new IllegalStateException(this.getStatusStringLocked()); + + _initial = false; + + Action action = nextAction(false); + if (LOG.isDebugEnabled()) + LOG.debug("nextAction(false) {} {}", action, toStringLocked()); + return action; + } + } + + private Action nextAction(boolean handling) + { + // Assume we can keep going, but exceptions are below + _state = State.HANDLING; + + if (_sendError) + { + switch (_requestState) + { + case BLOCKING: + case ASYNC: + case COMPLETE: + case DISPATCH: + case COMPLETING: + _requestState = RequestState.BLOCKING; + _sendError = false; + return Action.SEND_ERROR; + + default: + break; + } + } + + switch (_requestState) + { + case BLOCKING: + if (handling) + throw new IllegalStateException(getStatusStringLocked()); + _requestState = RequestState.COMPLETING; + return Action.COMPLETE; + + case ASYNC: + switch (_inputState) + { + case POSSIBLE: + _inputState = InputState.PRODUCING; + return Action.READ_PRODUCE; + case READY: + _inputState = InputState.IDLE; + return Action.READ_CALLBACK; + case REGISTER: + case PRODUCING: + _inputState = InputState.REGISTERED; + return Action.READ_REGISTER; + case IDLE: + case REGISTERED: + break; + default: + throw new IllegalStateException(getStatusStringLocked()); + } + + if (_asyncWritePossible) + { + _asyncWritePossible = false; + return Action.WRITE_CALLBACK; + } + + Scheduler scheduler = _channel.getScheduler(); + if (scheduler != null && _timeoutMs > 0 && !_event.hasTimeoutTask()) + _event.setTimeoutTask(scheduler.schedule(_event, _timeoutMs, TimeUnit.MILLISECONDS)); + _state = State.WAITING; + return Action.WAIT; + + case DISPATCH: + _requestState = RequestState.BLOCKING; + return Action.ASYNC_DISPATCH; + + case EXPIRE: + _requestState = RequestState.EXPIRING; + return Action.ASYNC_TIMEOUT; + + case EXPIRING: + if (handling) + throw new IllegalStateException(getStatusStringLocked()); + sendError(HttpStatus.INTERNAL_SERVER_ERROR_500, "AsyncContext timeout"); + // handle sendError immediately + _requestState = RequestState.BLOCKING; + _sendError = false; + return Action.SEND_ERROR; + + case COMPLETE: + _requestState = RequestState.COMPLETING; + return Action.COMPLETE; + + case COMPLETING: + _state = State.WAITING; + return Action.WAIT; + + case COMPLETED: + _state = State.IDLE; + return Action.TERMINATED; + + default: + throw new IllegalStateException(getStatusStringLocked()); + } + } + public void startAsync(AsyncContextEvent event) { final List lastAsyncListeners; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("startAsync {}", toStringLocked()); - if (_state != State.DISPATCHED || _async != Async.NOT_ASYNC) + if (_state != State.HANDLING || _requestState != RequestState.BLOCKING) throw new IllegalStateException(this.getStatusStringLocked()); - _async = Async.STARTED; + _requestState = RequestState.ASYNC; _event = event; lastAsyncListeners = _asyncListeners; _asyncListeners = null; @@ -359,216 +571,36 @@ public class HttpChannelState } } - public void asyncError(Throwable failure) - { - AsyncContextEvent event = null; - try (Locker.Lock lock = _locker.lock()) - { - switch (_state) - { - case IDLE: - case DISPATCHED: - case COMPLETING: - case COMPLETED: - case UPGRADED: - case ASYNC_IO: - case ASYNC_WOKEN: - case ASYNC_ERROR: - { - break; - } - case ASYNC_WAIT: - { - _event.addThrowable(failure); - _state = State.ASYNC_ERROR; - event = _event; - break; - } - default: - { - throw new IllegalStateException(getStatusStringLocked()); - } - } - } - - if (event != null) - { - cancelTimeout(event); - runInContext(event, _channel); - } - } - - /** - * Signal that the HttpConnection has finished handling the request. - * For blocking connectors, this call may block if the request has - * been suspended (startAsync called). - * - * @return next actions - * be handled again (eg because of a resume that happened before unhandle was called) - */ - protected Action unhandle() - { - boolean readInterested = false; - - try (Locker.Lock lock = _locker.lock()) - { - if (LOG.isDebugEnabled()) - LOG.debug("unhandle {}", toStringLocked()); - - switch (_state) - { - case COMPLETING: - case COMPLETED: - return Action.TERMINATED; - - case THROWN: - _state = State.DISPATCHED; - return Action.ERROR_DISPATCH; - - case DISPATCHED: - case ASYNC_IO: - case ASYNC_ERROR: - case ASYNC_WAIT: - break; - - default: - throw new IllegalStateException(this.getStatusStringLocked()); - } - - _initial = false; - switch (_async) - { - case COMPLETE: - _state = State.COMPLETING; - _async = Async.NOT_ASYNC; - return Action.COMPLETE; - - case DISPATCH: - _state = State.DISPATCHED; - _async = Async.NOT_ASYNC; - return Action.ASYNC_DISPATCH; - - case STARTED: - switch (_asyncRead) - { - case READY: - _state = State.ASYNC_IO; - _asyncRead = AsyncRead.IDLE; - return Action.READ_CALLBACK; - - case POSSIBLE: - _state = State.ASYNC_IO; - _asyncRead = AsyncRead.PRODUCING; - return Action.READ_PRODUCE; - - case REGISTER: - case PRODUCING: - _asyncRead = AsyncRead.REGISTERED; - readInterested = true; - break; - - case IDLE: - case REGISTERED: - break; - } - - if (_asyncWritePossible) - { - _state = State.ASYNC_IO; - _asyncWritePossible = false; - return Action.WRITE_CALLBACK; - } - else - { - _state = State.ASYNC_WAIT; - - Scheduler scheduler = _channel.getScheduler(); - if (scheduler != null && _timeoutMs > 0 && !_event.hasTimeoutTask()) - _event.setTimeoutTask(scheduler.schedule(_event, _timeoutMs, TimeUnit.MILLISECONDS)); - - return Action.WAIT; - } - - case EXPIRING: - // onTimeout callbacks still being called, so just WAIT - _state = State.ASYNC_WAIT; - return Action.WAIT; - - case EXPIRED: - // onTimeout handling is complete, but did not dispatch as - // we were handling. So do the error dispatch here - _state = State.DISPATCHED; - _async = Async.NOT_ASYNC; - return Action.ERROR_DISPATCH; - - case ERRORED: - _state = State.DISPATCHED; - _async = Async.NOT_ASYNC; - return Action.ERROR_DISPATCH; - - case NOT_ASYNC: - _state = State.COMPLETING; - return Action.COMPLETE; - - default: - _state = State.COMPLETING; - return Action.COMPLETE; - } - } - finally - { - if (readInterested) - _channel.onAsyncWaitForContent(); - } - } - public void dispatch(ServletContext context, String path) { boolean dispatch = false; AsyncContextEvent event; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("dispatch {} -> {}", toStringLocked(), path); - boolean started = false; - event = _event; - switch (_async) + switch (_requestState) { - case STARTED: - started = true; - break; + case ASYNC: case EXPIRING: - case ERRORING: - case ERRORED: break; default: throw new IllegalStateException(this.getStatusStringLocked()); } - _async = Async.DISPATCH; if (context != null) _event.setDispatchContext(context); if (path != null) _event.setDispatchPath(path); - if (started) + if (_requestState == RequestState.ASYNC && _state == State.WAITING) { - switch (_state) - { - case DISPATCHED: - case ASYNC_IO: - case ASYNC_WOKEN: - break; - case ASYNC_WAIT: - _state = State.ASYNC_WOKEN; - dispatch = true; - break; - default: - LOG.warn("async dispatched when complete {}", this); - break; - } + _state = State.WOKEN; + dispatch = true; } + _requestState = RequestState.DISPATCH; + event = _event; } cancelTimeout(event); @@ -576,23 +608,47 @@ public class HttpChannelState scheduleDispatch(); } + protected void timeout() + { + boolean dispatch = false; + synchronized (this) + { + if (LOG.isDebugEnabled()) + LOG.debug("Timeout {}", toStringLocked()); + + if (_requestState != RequestState.ASYNC) + return; + _requestState = RequestState.EXPIRE; + + if (_state == State.WAITING) + { + _state = State.WOKEN; + dispatch = true; + } + } + + if (dispatch) + { + if (LOG.isDebugEnabled()) + LOG.debug("Dispatch after async timeout {}", this); + scheduleDispatch(); + } + } + protected void onTimeout() { final List listeners; AsyncContextEvent event; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onTimeout {}", toStringLocked()); - - if (_async != Async.STARTED) - return; - _async = Async.EXPIRING; + if (_requestState != RequestState.EXPIRING || _state != State.HANDLING) + throw new IllegalStateException(toStringLocked()); event = _event; listeners = _asyncListeners; } - final AtomicReference error = new AtomicReference<>(); if (listeners != null) { Runnable task = new Runnable() @@ -610,11 +666,6 @@ public class HttpChannelState { LOG.warn(x + " while invoking onTimeout listener " + listener); LOG.debug(x); - Throwable failure = error.get(); - if (failure == null) - error.set(x); - else if (x != failure) - failure.addSuppressed(x); } } } @@ -628,86 +679,34 @@ public class HttpChannelState runInContext(event, task); } - - Throwable th = error.get(); - boolean dispatch = false; - try (Locker.Lock lock = _locker.lock()) - { - switch (_async) - { - case EXPIRING: - _async = th == null ? Async.EXPIRED : Async.ERRORING; - break; - - case COMPLETE: - case DISPATCH: - if (th != null) - { - LOG.ignore(th); - th = null; - } - break; - - default: - throw new IllegalStateException(); - } - - if (_state == State.ASYNC_WAIT) - { - _state = State.ASYNC_WOKEN; - dispatch = true; - } - } - - if (th != null) - { - if (LOG.isDebugEnabled()) - LOG.debug("Error after async timeout {}", this, th); - onError(th); - } - - if (dispatch) - { - if (LOG.isDebugEnabled()) - LOG.debug("Dispatch after async timeout {}", this); - scheduleDispatch(); - } } public void complete() { - - // just like resume, except don't set _dispatched=true; boolean handle = false; AsyncContextEvent event; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("complete {}", toStringLocked()); - boolean started = false; event = _event; - - switch (_async) + switch (_requestState) { - case STARTED: - started = true; - break; case EXPIRING: - case ERRORING: - case ERRORED: + case ASYNC: + _requestState = _sendError ? RequestState.BLOCKING : RequestState.COMPLETE; break; + case COMPLETE: return; default: throw new IllegalStateException(this.getStatusStringLocked()); } - _async = Async.COMPLETE; - - if (started && _state == State.ASYNC_WAIT) + if (_state == State.WAITING) { handle = true; - _state = State.ASYNC_WOKEN; + _state = State.WOKEN; } } @@ -716,228 +715,308 @@ public class HttpChannelState runInContext(event, _channel); } - public void errorComplete() + public void asyncError(Throwable failure) { - try (Locker.Lock lock = _locker.lock()) + // This method is called when an failure occurs asynchronously to + // normal handling. If the request is async, we arrange for the + // exception to be thrown from the normal handling loop and then + // actually handled by #thrownException + + AsyncContextEvent event = null; + synchronized (this) { if (LOG.isDebugEnabled()) - LOG.debug("error complete {}", toStringLocked()); + LOG.debug("asyncError " + toStringLocked(), failure); - _async = Async.COMPLETE; - _event.setDispatchContext(null); - _event.setDispatchPath(null); - } - - cancelTimeout(); - } - - protected void onError(Throwable th) - { - final List listeners; - final AsyncContextEvent event; - final Request baseRequest = _channel.getRequest(); - - int code = HttpStatus.INTERNAL_SERVER_ERROR_500; - String reason = null; - Throwable cause = _channel.unwrap(th, BadMessageException.class, UnavailableException.class); - if (cause instanceof BadMessageException) - { - BadMessageException bme = (BadMessageException)cause; - code = bme.getCode(); - reason = bme.getReason(); - } - else if (cause instanceof UnavailableException) - { - if (((UnavailableException)cause).isPermanent()) - code = HttpStatus.NOT_FOUND_404; - else - code = HttpStatus.SERVICE_UNAVAILABLE_503; - } - - try (Locker.Lock lock = _locker.lock()) - { - if (LOG.isDebugEnabled()) - LOG.debug("onError {} {}", toStringLocked(), th); - - // Set error on request. - if (_event != null) + if (_state == State.WAITING && _requestState == RequestState.ASYNC) { - _event.addThrowable(th); - _event.getSuppliedRequest().setAttribute(ERROR_STATUS_CODE, code); - _event.getSuppliedRequest().setAttribute(ERROR_EXCEPTION, th); - _event.getSuppliedRequest().setAttribute(ERROR_EXCEPTION_TYPE, th == null ? null : th.getClass()); - _event.getSuppliedRequest().setAttribute(ERROR_MESSAGE, reason); + _state = State.WOKEN; + _event.addThrowable(failure); + event = _event; } else { - Throwable error = (Throwable)baseRequest.getAttribute(ERROR_EXCEPTION); - if (error != null) - throw new IllegalStateException("Error already set", error); - baseRequest.setAttribute(ERROR_STATUS_CODE, code); - baseRequest.setAttribute(ERROR_EXCEPTION, th); - baseRequest.setAttribute(RequestDispatcher.ERROR_EXCEPTION_TYPE, th == null ? null : th.getClass()); - baseRequest.setAttribute(ERROR_MESSAGE, reason); - } - - // Are we blocking? - if (_async == Async.NOT_ASYNC) - { - // Only called from within HttpChannel Handling, so much be dispatched, let's stay dispatched! - if (_state == State.DISPATCHED) - { - _state = State.THROWN; - return; - } - throw new IllegalStateException(this.getStatusStringLocked()); - } - - // We are Async - _async = Async.ERRORING; - listeners = _asyncListeners; - event = _event; - } - - if (listeners != null) - { - Runnable task = new Runnable() - { - @Override - public void run() - { - for (AsyncListener listener : listeners) - { - try - { - listener.onError(event); - } - catch (Throwable x) - { - LOG.warn(x + " while invoking onError listener " + listener); - LOG.debug(x); - } - } - } - - @Override - public String toString() - { - return "onError"; - } - }; - runInContext(event, task); - } - - boolean dispatch = false; - try (Locker.Lock lock = _locker.lock()) - { - switch (_async) - { - case ERRORING: - { - // Still in this state ? The listeners did not invoke API methods - // and the container must provide a default error dispatch. - _async = Async.ERRORED; - break; - } - case DISPATCH: - case COMPLETE: - { - // The listeners called dispatch() or complete(). - break; - } - default: - { - throw new IllegalStateException(toString()); - } - } - - if (_state == State.ASYNC_WAIT) - { - _state = State.ASYNC_WOKEN; - dispatch = true; - } - } - - if (dispatch) - { - if (LOG.isDebugEnabled()) - LOG.debug("Dispatch after error {}", this); - scheduleDispatch(); - } - } - - protected void onComplete() - { - final List aListeners; - final AsyncContextEvent event; - - try (Locker.Lock lock = _locker.lock()) - { - if (LOG.isDebugEnabled()) - LOG.debug("onComplete {}", toStringLocked()); - - switch (_state) - { - case COMPLETING: - aListeners = _asyncListeners; - event = _event; - _state = State.COMPLETED; - _async = Async.NOT_ASYNC; - break; - - default: - throw new IllegalStateException(this.getStatusStringLocked()); + LOG.warn(failure.toString()); + LOG.debug(failure); } } if (event != null) { + cancelTimeout(event); + runInContext(event, _channel); + } + } + + protected void onError(Throwable th) + { + final AsyncContextEvent asyncEvent; + final List asyncListeners; + synchronized (this) + { + if (LOG.isDebugEnabled()) + LOG.debug("thrownException " + getStatusStringLocked(), th); + + // This can only be called from within the handle loop + if (_state != State.HANDLING) + throw new IllegalStateException(getStatusStringLocked()); + + // If sendError has already been called, we can only handle one failure at a time! + if (_sendError) + { + LOG.warn("unhandled due to prior sendError", th); + return; + } + + // Check async state to determine type of handling + switch (_requestState) + { + case BLOCKING: + // handle the exception with a sendError + sendError(th); + return; + + case DISPATCH: // Dispatch has already been called but we ignore and handle exception below + case COMPLETE: // Complete has already been called but we ignore and handle exception below + case ASYNC: + if (_asyncListeners == null || _asyncListeners.isEmpty()) + { + sendError(th); + return; + } + asyncEvent = _event; + asyncEvent.addThrowable(th); + asyncListeners = _asyncListeners; + break; + + default: + LOG.warn("unhandled in state " + _requestState, new IllegalStateException(th)); + return; + } + } + + // If we are async and have async listeners + // call onError + runInContext(asyncEvent, () -> + { + for (AsyncListener listener : asyncListeners) + { + try + { + listener.onError(asyncEvent); + } + catch (Throwable x) + { + LOG.warn(x + " while invoking onError listener " + listener); + LOG.debug(x); + } + } + }); + + // check the actions of the listeners + synchronized (this) + { + // If we are still async and nobody has called sendError + if (_requestState == RequestState.ASYNC && !_sendError) + // Then the listeners did not invoke API methods + // and the container must provide a default error dispatch. + sendError(th); + else + LOG.warn("unhandled in state " + _requestState, new IllegalStateException(th)); + } + } + + private void sendError(Throwable th) + { + // No sync as this is always called with lock held + + // Determine the actual details of the exception + final Request request = _channel.getRequest(); + final int code; + final String message; + Throwable cause = _channel.unwrap(th, BadMessageException.class, UnavailableException.class); + if (cause == null) + { + code = HttpStatus.INTERNAL_SERVER_ERROR_500; + message = th.toString(); + } + else if (cause instanceof BadMessageException) + { + BadMessageException bme = (BadMessageException)cause; + code = bme.getCode(); + message = bme.getReason(); + } + else if (cause instanceof UnavailableException) + { + message = cause.toString(); + if (((UnavailableException)cause).isPermanent()) + code = HttpStatus.NOT_FOUND_404; + else + code = HttpStatus.SERVICE_UNAVAILABLE_503; + } + else + { + code = HttpStatus.INTERNAL_SERVER_ERROR_500; + message = null; + } + + sendError(code, message); + + // No ISE, so good to modify request/state + request.setAttribute(ERROR_EXCEPTION, th); + request.setAttribute(ERROR_EXCEPTION_TYPE, th.getClass()); + // Ensure any async lifecycle is ended! + _requestState = RequestState.BLOCKING; + } + + public void sendError(int code, String message) + { + // This method is called by Response.sendError to organise for an error page to be generated when it is possible: + // + The response is reset and temporarily closed. + // + The details of the error are saved as request attributes + // + The _sendError boolean is set to true so that an ERROR_DISPATCH action will be generated: + // - after unhandle for sync + // - after both unhandle and complete for async + + final Request request = _channel.getRequest(); + final Response response = _channel.getResponse(); + if (message == null) + message = HttpStatus.getMessage(code); + + synchronized (this) + { + if (LOG.isDebugEnabled()) + LOG.debug("sendError {}", toStringLocked()); + + switch (_state) + { + case HANDLING: + case WOKEN: + case WAITING: + break; + default: + throw new IllegalStateException(getStatusStringLocked()); + } + if (_outputState != OutputState.OPEN) + throw new IllegalStateException("Response is " + _outputState); + + response.getHttpOutput().closedBySendError(); + response.setStatus(code); + + request.setAttribute(ErrorHandler.ERROR_CONTEXT, request.getErrorContext()); + request.setAttribute(ERROR_REQUEST_URI, request.getRequestURI()); + request.setAttribute(ERROR_SERVLET_NAME, request.getServletName()); + request.setAttribute(ERROR_STATUS_CODE, code); + request.setAttribute(ERROR_MESSAGE, message); + + _sendError = true; + if (_event != null) + { + Throwable cause = (Throwable)request.getAttribute(ERROR_EXCEPTION); + if (cause != null) + _event.addThrowable(cause); + } + } + } + + protected void completing() + { + synchronized (this) + { + if (LOG.isDebugEnabled()) + LOG.debug("completing {}", toStringLocked()); + + switch (_requestState) + { + case COMPLETED: + throw new IllegalStateException(getStatusStringLocked()); + default: + _requestState = RequestState.COMPLETING; + } + } + } + + protected void completed() + { + final List aListeners; + final AsyncContextEvent event; + boolean handle = false; + + synchronized (this) + { + if (LOG.isDebugEnabled()) + LOG.debug("completed {}", toStringLocked()); + + if (_requestState != RequestState.COMPLETING) + throw new IllegalStateException(this.getStatusStringLocked()); + + if (_event == null) + { + _requestState = RequestState.COMPLETED; + aListeners = null; + event = null; + if (_state == State.WAITING) + { + _state = State.WOKEN; + handle = true; + } + } + else + { + aListeners = _asyncListeners; + event = _event; + } + } + + if (event != null) + { + cancelTimeout(event); if (aListeners != null) { - Runnable callback = new Runnable() + runInContext(event, () -> { - @Override - public void run() + for (AsyncListener listener : aListeners) { - for (AsyncListener listener : aListeners) + try { - try - { - listener.onComplete(event); - } - catch (Throwable e) - { - LOG.warn(e + " while invoking onComplete listener " + listener); - LOG.debug(e); - } + listener.onComplete(event); + } + catch (Throwable e) + { + LOG.warn(e + " while invoking onComplete listener " + listener); + LOG.debug(e); } } - - @Override - public String toString() - { - return "onComplete"; - } - }; - - runInContext(event, callback); + }); } event.completed(); + + synchronized (this) + { + _requestState = RequestState.COMPLETED; + if (_state == State.WAITING) + { + _state = State.WOKEN; + handle = true; + } + } } + + if (handle) + _channel.handle(); } protected void recycle() { cancelTimeout(); - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("recycle {}", toStringLocked()); switch (_state) { - case DISPATCHED: - case ASYNC_IO: + case HANDLING: throw new IllegalStateException(getStatusStringLocked()); case UPGRADED: return; @@ -946,9 +1025,10 @@ public class HttpChannelState } _asyncListeners = null; _state = State.IDLE; - _async = Async.NOT_ASYNC; + _requestState = RequestState.BLOCKING; + _outputState = OutputState.OPEN; _initial = true; - _asyncRead = AsyncRead.IDLE; + _inputState = InputState.IDLE; _asyncWritePossible = false; _timeoutMs = DEFAULT_TIMEOUT; _event = null; @@ -958,7 +1038,7 @@ public class HttpChannelState public void upgrade() { cancelTimeout(); - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("upgrade {}", toStringLocked()); @@ -966,16 +1046,15 @@ public class HttpChannelState switch (_state) { case IDLE: - case COMPLETED: break; default: throw new IllegalStateException(getStatusStringLocked()); } _asyncListeners = null; _state = State.UPGRADED; - _async = Async.NOT_ASYNC; + _requestState = RequestState.BLOCKING; _initial = true; - _asyncRead = AsyncRead.IDLE; + _inputState = InputState.IDLE; _asyncWritePossible = false; _timeoutMs = DEFAULT_TIMEOUT; _event = null; @@ -990,7 +1069,7 @@ public class HttpChannelState protected void cancelTimeout() { final AsyncContextEvent event; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { event = _event; } @@ -1005,7 +1084,7 @@ public class HttpChannelState public boolean isIdle() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return _state == State.IDLE; } @@ -1013,15 +1092,16 @@ public class HttpChannelState public boolean isExpired() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { - return _async == Async.EXPIRED; + // TODO review + return _requestState == RequestState.EXPIRE || _requestState == RequestState.EXPIRING; } } public boolean isInitial() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return _initial; } @@ -1029,51 +1109,35 @@ public class HttpChannelState public boolean isSuspended() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { - return _state == State.ASYNC_WAIT || _state == State.DISPATCHED && _async == Async.STARTED; - } - } - - boolean isCompleting() - { - try (Locker.Lock lock = _locker.lock()) - { - return _state == State.COMPLETING; + return _state == State.WAITING || _state == State.HANDLING && _requestState == RequestState.ASYNC; } } boolean isCompleted() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { - return _state == State.COMPLETED; + return _requestState == RequestState.COMPLETED; } } public boolean isAsyncStarted() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { - if (_state == State.DISPATCHED) - return _async != Async.NOT_ASYNC; - return _async == Async.STARTED || _async == Async.EXPIRING; - } - } - - public boolean isAsyncComplete() - { - try (Locker.Lock lock = _locker.lock()) - { - return _async == Async.COMPLETE; + if (_state == State.HANDLING) + return _requestState != RequestState.BLOCKING; + return _requestState == RequestState.ASYNC || _requestState == RequestState.EXPIRING; } } public boolean isAsync() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { - return !_initial || _async != Async.NOT_ASYNC; + return !_initial || _requestState != RequestState.BLOCKING; } } @@ -1090,7 +1154,7 @@ public class HttpChannelState public ContextHandler getContextHandler() { final AsyncContextEvent event; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { event = _event; } @@ -1111,7 +1175,7 @@ public class HttpChannelState public ServletResponse getServletResponse() { final AsyncContextEvent event; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { event = _event; } @@ -1159,23 +1223,23 @@ public class HttpChannelState public void onReadUnready() { boolean interested = false; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onReadUnready {}", toStringLocked()); - switch (_asyncRead) + switch (_inputState) { case IDLE: case READY: - if (_state == State.ASYNC_WAIT) + if (_state == State.WAITING) { interested = true; - _asyncRead = AsyncRead.REGISTERED; + _inputState = InputState.REGISTERED; } else { - _asyncRead = AsyncRead.REGISTER; + _inputState = InputState.REGISTER; } break; @@ -1202,28 +1266,28 @@ public class HttpChannelState public boolean onContentAdded() { boolean woken = false; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onContentAdded {}", toStringLocked()); - switch (_asyncRead) + switch (_inputState) { case IDLE: case READY: break; case PRODUCING: - _asyncRead = AsyncRead.READY; + _inputState = InputState.READY; break; case REGISTER: case REGISTERED: - _asyncRead = AsyncRead.READY; - if (_state == State.ASYNC_WAIT) + _inputState = InputState.READY; + if (_state == State.WAITING) { woken = true; - _state = State.ASYNC_WOKEN; + _state = State.WOKEN; } break; @@ -1245,19 +1309,19 @@ public class HttpChannelState public boolean onReadReady() { boolean woken = false; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onReadReady {}", toStringLocked()); - switch (_asyncRead) + switch (_inputState) { case IDLE: - _asyncRead = AsyncRead.READY; - if (_state == State.ASYNC_WAIT) + _inputState = InputState.READY; + if (_state == State.WAITING) { woken = true; - _state = State.ASYNC_WOKEN; + _state = State.WOKEN; } break; @@ -1278,19 +1342,19 @@ public class HttpChannelState public boolean onReadPossible() { boolean woken = false; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onReadPossible {}", toStringLocked()); - switch (_asyncRead) + switch (_inputState) { case REGISTERED: - _asyncRead = AsyncRead.POSSIBLE; - if (_state == State.ASYNC_WAIT) + _inputState = InputState.POSSIBLE; + if (_state == State.WAITING) { woken = true; - _state = State.ASYNC_WOKEN; + _state = State.WOKEN; } break; @@ -1310,17 +1374,17 @@ public class HttpChannelState public boolean onReadEof() { boolean woken = false; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onEof {}", toStringLocked()); // Force read ready so onAllDataRead can be called - _asyncRead = AsyncRead.READY; - if (_state == State.ASYNC_WAIT) + _inputState = InputState.READY; + if (_state == State.WAITING) { woken = true; - _state = State.ASYNC_WOKEN; + _state = State.WOKEN; } } return woken; @@ -1330,15 +1394,15 @@ public class HttpChannelState { boolean wake = false; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onWritePossible {}", toStringLocked()); _asyncWritePossible = true; - if (_state == State.ASYNC_WAIT) + if (_state == State.WAITING) { - _state = State.ASYNC_WOKEN; + _state = State.WOKEN; wake = true; } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java index d81963c4058..80c31516e00 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java @@ -278,18 +278,8 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http } else if (filled < 0) { - switch (_channel.getState().getState()) - { - case COMPLETING: - case COMPLETED: - case IDLE: - case THROWN: - case ASYNC_ERROR: - getEndPoint().shutdownOutput(); - break; - default: - break; - } + if (_channel.getState().isIdle()) + getEndPoint().shutdownOutput(); break; } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java index bb5b1d58833..15f04a2c864 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java @@ -291,7 +291,8 @@ public class HttpInput extends ServletInputStream implements Runnable { BadMessageException bad = new BadMessageException(HttpStatus.REQUEST_TIMEOUT_408, String.format("Request content data rate < %d B/s", minRequestDataRate)); - _channelState.getHttpChannel().abort(bad); + if (_channelState.isResponseCommitted()) + _channelState.getHttpChannel().abort(bad); throw bad; } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java index e7dea9cba67..655e8cd5dbd 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java @@ -18,6 +18,7 @@ package org.eclipse.jetty.server; +import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; @@ -62,8 +63,31 @@ import org.eclipse.jetty.util.log.Logger; public class HttpOutput extends ServletOutputStream implements Runnable { private static final String LSTRING_FILE = "javax.servlet.LocalStrings"; + private static final Callback BLOCKING_CLOSE_CALLBACK = new Callback() {}; private static ResourceBundle lStrings = ResourceBundle.getBundle(LSTRING_FILE); + /* + ACTION OPEN ASYNC READY PENDING UNREADY CLOSING CLOSED + -------------------------------------------------------------------------------------------------- + setWriteListener() READY->owp ise ise ise ise ise ise + write() OPEN ise PENDING wpe wpe eof eof + flush() OPEN ise PENDING wpe wpe eof eof + close() CLOSING CLOSING CLOSING CLOSED CLOSED CLOSING CLOSED + isReady() OPEN:true READY:true READY:true UNREADY:false UNREADY:false CLOSED:true CLOSED:true + write completed - - - ASYNC READY->owp CLOSED - + */ + enum State + { + OPEN, // Open in blocking mode + ASYNC, // Open in async mode + READY, // isReady() has returned true + PENDING, // write operating in progress + UNREADY, // write operating in progress, isReady has returned false + ERROR, // An error has occured + CLOSING, // Asynchronous close in progress + CLOSED // Closed + } + /** * The HttpOutput.Interceptor is a single intercept point for all * output written to the HttpOutput: via writer; via output stream; @@ -129,6 +153,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable private static Logger LOG = Log.getLogger(HttpOutput.class); private static final ThreadLocal _encoder = new ThreadLocal<>(); + private final AtomicReference _state = new AtomicReference<>(State.OPEN); private final HttpChannel _channel; private final SharedBlockingCallback _writeBlocker; private Interceptor _interceptor; @@ -140,23 +165,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable private int _commitSize; private WriteListener _writeListener; private volatile Throwable _onError; - - /* - ACTION OPEN ASYNC READY PENDING UNREADY CLOSED - ------------------------------------------------------------------------------------------- - setWriteListener() READY->owp ise ise ise ise ise - write() OPEN ise PENDING wpe wpe eof - flush() OPEN ise PENDING wpe wpe eof - close() CLOSED CLOSED CLOSED CLOSED CLOSED CLOSED - isReady() OPEN:true READY:true READY:true UNREADY:false UNREADY:false CLOSED:true - write completed - - - ASYNC READY->owp - - */ - private enum OutputState - { - OPEN, ASYNC, READY, PENDING, UNREADY, ERROR, CLOSED - } - - private final AtomicReference _state = new AtomicReference<>(OutputState.OPEN); + private Callback _closeCallback; public HttpOutput(HttpChannel channel) { @@ -200,7 +209,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable public void reopen() { - _state.set(OutputState.OPEN); + _state.set(State.OPEN); } private boolean isLastContentToWrite(int len) @@ -256,28 +265,78 @@ public class HttpOutput extends ServletOutputStream implements Runnable _channel.abort(failure); } - @Override - public void close() + public void closedBySendError() { while (true) { - OutputState state = _state.get(); + State state = _state.get(); switch (state) { + case OPEN: + case READY: + case ASYNC: + if (!_state.compareAndSet(state, State.CLOSED)) + continue; + return; + + default: + throw new IllegalStateException(state.toString()); + } + } + } + + public void close(Closeable wrapper, Callback callback) + { + _closeCallback = callback; + try + { + if (wrapper != null) + wrapper.close(); + if (!isClosed()) + close(); + } + catch (Throwable th) + { + closed(); + if (_closeCallback == null) + LOG.ignore(th); + else + callback.failed(th); + } + finally + { + if (_closeCallback != null) + callback.succeeded(); + _closeCallback = null; + } + } + + @Override + public void close() + { + Callback closeCallback = _closeCallback == null ? BLOCKING_CLOSE_CALLBACK : _closeCallback; + + while (true) + { + State state = _state.get(); + switch (state) + { + case CLOSING: case CLOSED: { + _closeCallback = null; + closeCallback.succeeded(); return; } case ASYNC: { // A close call implies a write operation, thus in asynchronous mode // a call to isReady() that returned true should have been made. - // However it is desirable to allow a close at any time, specially if - // complete is called. Thus we simulate a call to isReady here, assuming - // that we can transition to READY. - if (!_state.compareAndSet(state, OutputState.READY)) - continue; - break; + // However it is desirable to allow a close at any time, specially if + // complete is called. Thus we simulate a call to isReady here, by + // trying to move to READY state. Either way we continue. + _state.compareAndSet(state, State.READY); + continue; } case UNREADY: case PENDING: @@ -288,34 +347,45 @@ public class HttpOutput extends ServletOutputStream implements Runnable // complete is called. Because the prior write has not yet completed // and/or isReady has not been called, this close is allowed, but will // abort the response. - if (!_state.compareAndSet(state, OutputState.CLOSED)) + if (!_state.compareAndSet(state, State.CLOSED)) continue; IOException ex = new IOException("Closed while Pending/Unready"); LOG.warn(ex.toString()); LOG.debug(ex); abort(ex); + _closeCallback = null; + closeCallback.failed(ex); return; } default: { - if (!_state.compareAndSet(state, OutputState.CLOSED)) + if (!_state.compareAndSet(state, State.CLOSING)) continue; // Do a normal close by writing the aggregate buffer or an empty buffer. If we are // not including, then indicate this is the last write. try { - write(BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER, !_channel.getResponse().isIncluding()); + ByteBuffer content = BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER; + if (closeCallback == BLOCKING_CLOSE_CALLBACK) + { + // Do a blocking close + write(content, !_channel.getResponse().isIncluding()); + _closeCallback = null; + closeCallback.succeeded(); + } + else + { + _closeCallback = null; + write(content, !_channel.getResponse().isIncluding(), closeCallback); + } } catch (IOException x) { LOG.ignore(x); // Ignore it, it's been already logged in write(). + _closeCallback = null; + closeCallback.failed(x); } - finally - { - releaseBuffer(); - } - // Return even if an exception is thrown by write(). return; } } @@ -326,11 +396,11 @@ public class HttpOutput extends ServletOutputStream implements Runnable * Called to indicate that the last write has been performed. * It updates the state and performs cleanup operations. */ - void closed() + public void closed() { while (true) { - OutputState state = _state.get(); + State state = _state.get(); switch (state) { case CLOSED: @@ -339,15 +409,16 @@ public class HttpOutput extends ServletOutputStream implements Runnable } case UNREADY: { - if (_state.compareAndSet(state, OutputState.ERROR)) + if (_state.compareAndSet(state, State.ERROR)) _writeListener.onError(_onError == null ? new EofException("Async closed") : _onError); break; } default: { - if (!_state.compareAndSet(state, OutputState.CLOSED)) + if (!_state.compareAndSet(state, State.CLOSED)) break; + // Just make sure write and output stream really are closed try { _channel.getResponse().closeOutput(); @@ -369,6 +440,18 @@ public class HttpOutput extends ServletOutputStream implements Runnable } } + public ByteBuffer getBuffer() + { + return _aggregate; + } + + public ByteBuffer acquireBuffer() + { + if (_aggregate == null) + _aggregate = _channel.getByteBufferPool().acquire(getBufferSize(), _interceptor.isOptimizedForDirectBuffers()); + return _aggregate; + } + private void releaseBuffer() { if (_aggregate != null) @@ -380,7 +463,14 @@ public class HttpOutput extends ServletOutputStream implements Runnable public boolean isClosed() { - return _state.get() == OutputState.CLOSED; + switch (_state.get()) + { + case CLOSING: + case CLOSED: + return true; + default: + return false; + } } public boolean isAsync() @@ -402,7 +492,8 @@ public class HttpOutput extends ServletOutputStream implements Runnable { while (true) { - switch (_state.get()) + State state = _state.get(); + switch (state) { case OPEN: write(BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER, false); @@ -412,25 +503,24 @@ public class HttpOutput extends ServletOutputStream implements Runnable throw new IllegalStateException("isReady() not called"); case READY: - if (!_state.compareAndSet(OutputState.READY, OutputState.PENDING)) + if (!_state.compareAndSet(state, State.PENDING)) continue; new AsyncFlush().iterate(); return; - case PENDING: - return; - case UNREADY: throw new WritePendingException(); case ERROR: throw new EofException(_onError); + case PENDING: + case CLOSING: case CLOSED: return; default: - throw new IllegalStateException(); + throw new IllegalStateException(state.toString()); } } } @@ -441,7 +531,8 @@ public class HttpOutput extends ServletOutputStream implements Runnable // Async or Blocking ? while (true) { - switch (_state.get()) + State state = _state.get(); + switch (state) { case OPEN: // process blocking below @@ -451,15 +542,14 @@ public class HttpOutput extends ServletOutputStream implements Runnable throw new IllegalStateException("isReady() not called"); case READY: - if (!_state.compareAndSet(OutputState.READY, OutputState.PENDING)) + if (!_state.compareAndSet(state, State.PENDING)) continue; // Should we aggregate? boolean last = isLastContentToWrite(len); if (!last && len <= _commitSize) { - if (_aggregate == null) - _aggregate = _channel.getByteBufferPool().acquire(getBufferSize(), _interceptor.isOptimizedForDirectBuffers()); + acquireBuffer(); // YES - fill the aggregate with content from the buffer int filled = BufferUtil.fill(_aggregate, b, off, len); @@ -467,8 +557,8 @@ public class HttpOutput extends ServletOutputStream implements Runnable // return if we are not complete, not full and filled all the content if (filled == len && !BufferUtil.isFull(_aggregate)) { - if (!_state.compareAndSet(OutputState.PENDING, OutputState.ASYNC)) - throw new IllegalStateException(); + if (!_state.compareAndSet(State.PENDING, State.ASYNC)) + throw new IllegalStateException(_state.get().toString()); return; } @@ -488,11 +578,12 @@ public class HttpOutput extends ServletOutputStream implements Runnable case ERROR: throw new EofException(_onError); + case CLOSING: case CLOSED: throw new EofException("Closed"); default: - throw new IllegalStateException(); + throw new IllegalStateException(state.toString()); } break; } @@ -504,8 +595,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable boolean last = isLastContentToWrite(len); if (!last && len <= _commitSize) { - if (_aggregate == null) - _aggregate = _channel.getByteBufferPool().acquire(capacity, _interceptor.isOptimizedForDirectBuffers()); + acquireBuffer(); // YES - fill the aggregate with content from the buffer int filled = BufferUtil.fill(_aggregate, b, off, len); @@ -554,9 +644,6 @@ public class HttpOutput extends ServletOutputStream implements Runnable { write(BufferUtil.EMPTY_BUFFER, true); } - - if (last) - closed(); } public void write(ByteBuffer buffer) throws IOException @@ -566,7 +653,8 @@ public class HttpOutput extends ServletOutputStream implements Runnable // Async or Blocking ? while (true) { - switch (_state.get()) + State state = _state.get(); + switch (state) { case OPEN: // process blocking below @@ -576,7 +664,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable throw new IllegalStateException("isReady() not called"); case READY: - if (!_state.compareAndSet(OutputState.READY, OutputState.PENDING)) + if (!_state.compareAndSet(state, State.PENDING)) continue; // Do the asynchronous writing from the callback @@ -591,11 +679,12 @@ public class HttpOutput extends ServletOutputStream implements Runnable case ERROR: throw new EofException(_onError); + case CLOSING: case CLOSED: throw new EofException("Closed"); default: - throw new IllegalStateException(); + throw new IllegalStateException(state.toString()); } break; } @@ -613,9 +702,6 @@ public class HttpOutput extends ServletOutputStream implements Runnable write(buffer, last); else if (last) write(BufferUtil.EMPTY_BUFFER, true); - - if (last) - closed(); } @Override @@ -630,34 +716,28 @@ public class HttpOutput extends ServletOutputStream implements Runnable switch (_state.get()) { case OPEN: - if (_aggregate == null) - _aggregate = _channel.getByteBufferPool().acquire(getBufferSize(), _interceptor.isOptimizedForDirectBuffers()); + acquireBuffer(); BufferUtil.append(_aggregate, (byte)b); // Check if all written or full if (complete || BufferUtil.isFull(_aggregate)) - { write(_aggregate, complete); - if (complete) - closed(); - } break; case ASYNC: throw new IllegalStateException("isReady() not called"); case READY: - if (!_state.compareAndSet(OutputState.READY, OutputState.PENDING)) + if (!_state.compareAndSet(State.READY, State.PENDING)) continue; - if (_aggregate == null) - _aggregate = _channel.getByteBufferPool().acquire(getBufferSize(), _interceptor.isOptimizedForDirectBuffers()); + acquireBuffer(); BufferUtil.append(_aggregate, (byte)b); // Check if all written or full if (!complete && !BufferUtil.isFull(_aggregate)) { - if (!_state.compareAndSet(OutputState.PENDING, OutputState.ASYNC)) + if (!_state.compareAndSet(State.PENDING, State.ASYNC)) throw new IllegalStateException(); return; } @@ -673,6 +753,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable case ERROR: throw new EofException(_onError); + case CLOSING: case CLOSED: throw new EofException("Closed"); @@ -810,7 +891,6 @@ public class HttpOutput extends ServletOutputStream implements Runnable _written += content.remaining(); write(content, true); - closed(); } /** @@ -966,7 +1046,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable switch (_state.get()) { case OPEN: - if (!_state.compareAndSet(OutputState.OPEN, OutputState.PENDING)) + if (!_state.compareAndSet(State.OPEN, State.PENDING)) continue; break; @@ -974,6 +1054,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable callback.failed(new EofException(_onError)); return; + case CLOSING: case CLOSED: callback.failed(new EofException("Closed")); return; @@ -1073,6 +1154,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable _onError = null; _firstByteTimeStamp = -1; _flushed = 0; + _closeCallback = null; reopen(); } @@ -1082,7 +1164,6 @@ public class HttpOutput extends ServletOutputStream implements Runnable if (BufferUtil.hasContent(_aggregate)) BufferUtil.clear(_aggregate); _written = 0; - reopen(); } @Override @@ -1091,7 +1172,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable if (!_channel.getState().isAsync()) throw new IllegalStateException("!ASYNC"); - if (_state.compareAndSet(OutputState.OPEN, OutputState.READY)) + if (_state.compareAndSet(State.OPEN, State.READY)) { _writeListener = writeListener; if (_channel.getState().onWritePossible()) @@ -1109,30 +1190,25 @@ public class HttpOutput extends ServletOutputStream implements Runnable switch (_state.get()) { case OPEN: + case READY: + case ERROR: + case CLOSING: + case CLOSED: return true; case ASYNC: - if (!_state.compareAndSet(OutputState.ASYNC, OutputState.READY)) + if (!_state.compareAndSet(State.ASYNC, State.READY)) continue; return true; - case READY: - return true; - case PENDING: - if (!_state.compareAndSet(OutputState.PENDING, OutputState.UNREADY)) + if (!_state.compareAndSet(State.PENDING, State.UNREADY)) continue; return false; case UNREADY: return false; - case ERROR: - return true; - - case CLOSED: - return true; - default: throw new IllegalStateException(); } @@ -1144,12 +1220,13 @@ public class HttpOutput extends ServletOutputStream implements Runnable { while (true) { - OutputState state = _state.get(); + State state = _state.get(); if (_onError != null) { switch (state) { + case CLOSING: case CLOSED: case ERROR: { @@ -1158,7 +1235,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable } default: { - if (_state.compareAndSet(state, OutputState.ERROR)) + if (_state.compareAndSet(state, State.ERROR)) { Throwable th = _onError; _onError = null; @@ -1234,16 +1311,16 @@ public class HttpOutput extends ServletOutputStream implements Runnable { while (true) { - OutputState last = _state.get(); + State last = _state.get(); switch (last) { case PENDING: - if (!_state.compareAndSet(OutputState.PENDING, OutputState.ASYNC)) + if (!_state.compareAndSet(State.PENDING, State.ASYNC)) continue; break; case UNREADY: - if (!_state.compareAndSet(OutputState.UNREADY, OutputState.READY)) + if (!_state.compareAndSet(State.UNREADY, State.READY)) continue; if (_last) closed(); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java index 76388346ccb..a57a550d80b 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -759,10 +759,18 @@ public class Request implements HttpServletRequest } /** - * @return The current {@link Context context} used for this request, or null if {@link #setContext} has not yet been called. + * @return The current {@link Context context} used for this error handling for this request. If the request is asynchronous, + * then it is the context that called async. Otherwise it is the last non-null context passed to #setContext */ public Context getErrorContext() { + if (isAsyncStarted()) + { + ContextHandler handler = _channel.getState().getContextHandler(); + if (handler != null) + return handler.getServletContext(); + } + return _errorContext; } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index 10fc3f33d25..04870c50949 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -18,18 +18,18 @@ package org.eclipse.jetty.server; +import java.io.Closeable; import java.io.IOException; import java.io.PrintWriter; import java.nio.channels.IllegalSelectorException; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; -import java.util.List; +import java.util.Iterator; import java.util.ListIterator; import java.util.Locale; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; -import javax.servlet.RequestDispatcher; import javax.servlet.ServletOutputStream; import javax.servlet.ServletResponse; import javax.servlet.ServletResponseWrapper; @@ -57,9 +57,8 @@ import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.PreEncodedHttpField; import org.eclipse.jetty.io.RuntimeIOException; -import org.eclipse.jetty.server.handler.ContextHandler; -import org.eclipse.jetty.server.handler.ErrorHandler; import org.eclipse.jetty.server.session.SessionHandler; +import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.log.Log; @@ -378,70 +377,35 @@ public class Response implements HttpServletResponse sendError(sc, null); } + /** + * Send an error response. + *

In addition to the servlet standard handling, this method supports some additional codes:

+ *
+ *
102
Send a partial PROCESSING response and allow additional responses
+ *
-1
Abort the HttpChannel and close the connection/stream
+ *
+ * @param code The error code + * @param message The message + * @throws IOException If an IO problem occurred sending the error response. + */ @Override public void sendError(int code, String message) throws IOException { if (isIncluding()) return; - if (isCommitted()) - { - if (LOG.isDebugEnabled()) - LOG.debug("Aborting on sendError on committed response {} {}", code, message); - code = -1; - } - else - resetBuffer(); - switch (code) { case -1: - _channel.abort(new IOException()); - return; - case 102: + _channel.abort(new IOException(message)); + break; + case HttpStatus.PROCESSING_102: sendProcessing(); - return; + break; default: + _channel.getState().sendError(code, message); break; } - - _outputType = OutputType.NONE; - setContentType(null); - setCharacterEncoding(null); - setHeader(HttpHeader.EXPIRES, null); - setHeader(HttpHeader.LAST_MODIFIED, null); - setHeader(HttpHeader.CACHE_CONTROL, null); - setHeader(HttpHeader.CONTENT_TYPE, null); - setHeader(HttpHeader.CONTENT_LENGTH, null); - - setStatus(code); - - Request request = _channel.getRequest(); - Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION); - if (message == null) - { - _reason = HttpStatus.getMessage(code); - message = cause == null ? _reason : cause.toString(); - } - else - _reason = message; - - // If we are allowed to have a body, then produce the error page. - if (code != SC_NO_CONTENT && code != SC_NOT_MODIFIED && - code != SC_PARTIAL_CONTENT && code >= SC_OK) - { - ContextHandler.Context context = request.getErrorContext(); - ContextHandler contextHandler = context == null ? _channel.getState().getContextHandler() : context.getContextHandler(); - request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, code); - request.setAttribute(RequestDispatcher.ERROR_MESSAGE, message); - request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI, request.getRequestURI()); - request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME, request.getServletName()); - ErrorHandler errorHandler = ErrorHandler.getErrorHandler(_channel.getServer(), contextHandler); - if (errorHandler != null) - errorHandler.handle(null, request, request, this); - } - if (!request.isAsyncStarted()) - closeOutput(); } /** @@ -653,8 +617,11 @@ public class Response implements HttpServletResponse throw new IllegalArgumentException(); if (!isIncluding()) { + // Null the reason only if the status is different. This allows + // a specific reason to be sent with setStatusWithReason followed by sendError. + if (_status != sc) + _reason = null; _status = sc; - _reason = null; } } @@ -717,6 +684,11 @@ public class Response implements HttpServletResponse return _outputType == OutputType.STREAM; } + public boolean isWritingOrStreaming() + { + return isWriting() || isStreaming(); + } + @Override public PrintWriter getWriter() throws IOException { @@ -825,21 +797,15 @@ public class Response implements HttpServletResponse public void closeOutput() throws IOException { - switch (_outputType) - { - case WRITER: - _writer.close(); - if (!_out.isClosed()) - _out.close(); - break; - case STREAM: - if (!_out.isClosed()) - getOutputStream().close(); - break; - default: - if (!_out.isClosed()) - _out.close(); - } + if (_outputType == OutputType.WRITER) + _writer.close(); + if (!_out.isClosed()) + _out.close(); + } + + public void closeOutput(Callback callback) + { + _out.close((_outputType == OutputType.WRITER) ? _writer : _out, callback); } public long getLongContentLength() @@ -1029,19 +995,20 @@ public class Response implements HttpServletResponse @Override public void reset() { - reset(false); - } - - public void reset(boolean preserveCookies) - { - resetForForward(); _status = 200; _reason = null; + _out.resetBuffer(); + _outputType = OutputType.NONE; _contentLength = -1; + _contentType = null; + _mimeType = null; + _characterEncoding = null; + _encodingFrom = EncodingFrom.NOT_SET; - List cookies = preserveCookies ? _fields.getFields(HttpHeader.SET_COOKIE) : null; + // Clear all response headers _fields.clear(); + // recreate necessary connection related fields for (String value : _channel.getRequest().getHttpFields().getCSV(HttpHeader.CONNECTION, false)) { HttpHeaderValue cb = HttpHeaderValue.CACHE.get(value); @@ -1064,21 +1031,57 @@ public class Response implements HttpServletResponse } } - if (preserveCookies) - cookies.forEach(_fields::add); - else + // recreate session cookies + Request request = getHttpChannel().getRequest(); + HttpSession session = request.getSession(false); + if (session != null && session.isNew()) { - Request request = getHttpChannel().getRequest(); - HttpSession session = request.getSession(false); - if (session != null && session.isNew()) + SessionHandler sh = request.getSessionHandler(); + if (sh != null) { - SessionHandler sh = request.getSessionHandler(); - if (sh != null) - { - HttpCookie c = sh.getSessionCookie(session, request.getContextPath(), request.isSecure()); - if (c != null) - addCookie(c); - } + HttpCookie c = sh.getSessionCookie(session, request.getContextPath(), request.isSecure()); + if (c != null) + addCookie(c); + } + } + } + + public void resetContent() + { + _out.resetBuffer(); + _outputType = OutputType.NONE; + _contentLength = -1; + _contentType = null; + _mimeType = null; + _characterEncoding = null; + _encodingFrom = EncodingFrom.NOT_SET; + + // remove the content related response headers and keep all others + for (Iterator i = getHttpFields().iterator(); i.hasNext(); ) + { + HttpField field = i.next(); + if (field.getHeader() == null) + continue; + + switch (field.getHeader()) + { + case CONTENT_TYPE: + case CONTENT_LENGTH: + case CONTENT_ENCODING: + case CONTENT_LANGUAGE: + case CONTENT_RANGE: + case CONTENT_MD5: + case CONTENT_LOCATION: + case TRANSFER_ENCODING: + case CACHE_CONTROL: + case LAST_MODIFIED: + case EXPIRES: + case ETAG: + case DATE: + case VARY: + i.remove(); + continue; + default: } } } @@ -1093,6 +1096,7 @@ public class Response implements HttpServletResponse public void resetBuffer() { _out.resetBuffer(); + _out.reopen(); } public void setTrailers(Supplier trailers) @@ -1133,6 +1137,9 @@ public class Response implements HttpServletResponse @Override public boolean isCommitted() { + // If we are in sendError state, we pretend to be committed + if (_channel.isSendError()) + return true; return _channel.isCommitted(); } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java index 72ac6bf52b7..93e3ff024bb 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java @@ -485,10 +485,16 @@ public class Server extends HandlerWrapper implements Attributes if (HttpMethod.OPTIONS.is(request.getMethod()) || "*".equals(target)) { if (!HttpMethod.OPTIONS.is(request.getMethod())) + { + request.setHandled(true); response.sendError(HttpStatus.BAD_REQUEST_400); - handleOptions(request, response); - if (!request.isHandled()) - handle(target, request, request, response); + } + else + { + handleOptions(request, response); + if (!request.isHandled()) + handle(target, request, request, response); + } } else handle(target, request, request, response); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java index e11f4047912..4297a04a031 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java @@ -1106,7 +1106,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu case UNAVAILABLE: baseRequest.setHandled(true); response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); - return true; + return false; default: if ((DispatcherType.REQUEST.equals(dispatch) && baseRequest.isHandled())) return false; @@ -1141,8 +1141,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu if (oldContext != _scontext) { // check the target. - if (DispatcherType.REQUEST.equals(dispatch) || DispatcherType.ASYNC.equals(dispatch) || - DispatcherType.ERROR.equals(dispatch) && baseRequest.getHttpChannelState().isAsync()) + if (DispatcherType.REQUEST.equals(dispatch) || DispatcherType.ASYNC.equals(dispatch)) { if (_compactPath) target = URIUtil.compactPath(target); @@ -1279,29 +1278,11 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu if (new_context) requestInitialized(baseRequest, request); - switch (dispatch) + if (dispatch == DispatcherType.REQUEST && isProtectedTarget(target)) { - case REQUEST: - if (isProtectedTarget(target)) - { - response.sendError(HttpServletResponse.SC_NOT_FOUND); - baseRequest.setHandled(true); - return; - } - break; - - case ERROR: - // If this is already a dispatch to an error page, proceed normally - if (Boolean.TRUE.equals(baseRequest.getAttribute(Dispatcher.__ERROR_DISPATCH))) - break; - - // We can just call doError here. If there is no error page, then one will - // be generated. If there is an error page, then a RequestDispatcher will be - // used to route the request through appropriate filters etc. - doError(target, baseRequest, request, response); - return; - default: - break; + baseRequest.setHandled(true); + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; } nextHandle(target, baseRequest, request, response); 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 09a7738f868..14700779b34 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 @@ -19,28 +19,29 @@ package org.eclipse.jetty.server.handler; import java.io.IOException; +import java.io.OutputStreamWriter; import java.io.PrintWriter; -import java.io.StringWriter; import java.io.Writer; +import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.List; import javax.servlet.RequestDispatcher; -import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; -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.io.ByteBufferOutputStream; import org.eclipse.jetty.server.Dispatcher; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.QuotedStringTokenizer; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -54,10 +55,13 @@ import org.eclipse.jetty.util.log.Logger; */ public class ErrorHandler extends AbstractHandler { + // TODO This classes API needs to be majorly refactored/cleanup in jetty-10 private static final Logger LOG = Log.getLogger(ErrorHandler.class); public static final String ERROR_PAGE = "org.eclipse.jetty.server.error_page"; + public static final String ERROR_CONTEXT = "org.eclipse.jetty.server.error_context"; boolean _showStacks = true; + boolean _disableStacks = false; boolean _showMessageInTitle = true; String _cacheControl = "must-revalidate,no-cache,no-store"; @@ -65,6 +69,19 @@ public class ErrorHandler extends AbstractHandler { } + public boolean errorPageForMethod(String method) + { + switch (method) + { + case "GET": + case "POST": + case "HEAD": + return true; + default: + return false; + } + } + /* * @see org.eclipse.jetty.server.server.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int) */ @@ -77,65 +94,14 @@ public class ErrorHandler extends AbstractHandler @Override public void doError(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { - String method = request.getMethod(); - if (!HttpMethod.GET.is(method) && !HttpMethod.POST.is(method) && !HttpMethod.HEAD.is(method)) - { - baseRequest.setHandled(true); - return; - } + String cacheControl = getCacheControl(); + if (cacheControl != null) + response.setHeader(HttpHeader.CACHE_CONTROL.asString(), cacheControl); - if (this instanceof ErrorPageMapper) - { - String errorPage = ((ErrorPageMapper)this).getErrorPage(request); - if (errorPage != null) - { - String oldErrorPage = (String)request.getAttribute(ERROR_PAGE); - ContextHandler.Context context = baseRequest.getErrorContext(); - if (context == null) - context = ContextHandler.getCurrentContext(); - if (context == null) - { - LOG.warn("No ServletContext for error page {}", errorPage); - } - else if (oldErrorPage != null && oldErrorPage.equals(errorPage)) - { - LOG.warn("Error page loop {}", errorPage); - } - else - { - request.setAttribute(ERROR_PAGE, errorPage); - - Dispatcher dispatcher = (Dispatcher)context.getRequestDispatcher(errorPage); - try - { - if (LOG.isDebugEnabled()) - LOG.debug("error page dispatch {}->{}", errorPage, dispatcher); - if (dispatcher != null) - { - dispatcher.error(request, response); - return; - } - LOG.warn("No error page found " + errorPage); - } - catch (ServletException e) - { - LOG.warn(Log.EXCEPTION, e); - return; - } - } - } - else - { - if (LOG.isDebugEnabled()) - { - LOG.debug("No Error Page mapping for request({} {}) (using default)", request.getMethod(), request.getRequestURI()); - } - } - } - - if (_cacheControl != null) - response.setHeader(HttpHeader.CACHE_CONTROL.asString(), _cacheControl); - generateAcceptableResponse(baseRequest, request, response, response.getStatus(), baseRequest.getResponse().getReason()); + String message = (String)request.getAttribute(Dispatcher.ERROR_MESSAGE); + if (message == null) + message = baseRequest.getResponse().getReason(); + generateAcceptableResponse(baseRequest, request, response, response.getStatus(), message); } /** @@ -144,7 +110,7 @@ public class ErrorHandler extends AbstractHandler * acceptable to the user-agent. The Accept header is evaluated in * quality order and the method * {@link #generateAcceptableResponse(Request, HttpServletRequest, HttpServletResponse, int, String, String)} - * is called for each mimetype until {@link Request#isHandled()} is true.

+ * is called for each mimetype until the response is written to or committed.

* * @param baseRequest The base request * @param request The servlet request (may be wrapped) @@ -167,11 +133,10 @@ public class ErrorHandler extends AbstractHandler for (String mimeType : acceptable) { generateAcceptableResponse(baseRequest, request, response, code, message, mimeType); - if (response.isCommitted() || baseRequest.getResponse().isWriting() || baseRequest.getResponse().isStreaming()) + if (response.isCommitted() || baseRequest.getResponse().isWritingOrStreaming()) break; } } - baseRequest.getResponse().closeOutput(); } /** @@ -192,6 +157,7 @@ public class ErrorHandler extends AbstractHandler * @return A {@link Writer} if there is a known acceptable charset or null * @throws IOException if a Writer cannot be returned */ + @Deprecated protected Writer getAcceptableWriter(Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { @@ -225,33 +191,132 @@ public class ErrorHandler extends AbstractHandler *

This method is called for each mime type in the users agent's * Accept header, until {@link Request#isHandled()} is true and a * response of the appropriate type is generated. + *

+ *

The default implementation handles "text/html", "text/*" and "*/*". + * The method can be overridden to handle other types. Implementations must + * immediate produce a response and may not be async. + *

* * @param baseRequest The base request * @param request The servlet request (may be wrapped) * @param response The response (may be wrapped) * @param code the http error code * @param message the http error message - * @param mimeType The mimetype to generate (may be */*or other wildcard) + * @param contentType The mimetype to generate (may be */*or other wildcard) * @throws IOException if a response cannot be generated */ - protected void generateAcceptableResponse(Request baseRequest, HttpServletRequest request, HttpServletResponse response, int code, String message, String mimeType) + protected void generateAcceptableResponse(Request baseRequest, HttpServletRequest request, HttpServletResponse response, int code, String message, String contentType) throws IOException { - switch (mimeType) + // We can generate an acceptable contentType, but can we generate an acceptable charset? + // TODO refactor this in jetty-10 to be done in the other calling loop + Charset charset = null; + List acceptable = baseRequest.getHttpFields().getQualityCSV(HttpHeader.ACCEPT_CHARSET); + if (!acceptable.isEmpty()) + { + for (String name : acceptable) + { + if ("*".equals(name)) + { + charset = StandardCharsets.UTF_8; + break; + } + + try + { + charset = Charset.forName(name); + } + catch (Exception e) + { + LOG.ignore(e); + } + } + if (charset == null) + return; + } + + MimeTypes.Type type; + switch (contentType) { case "text/html": case "text/*": case "*/*": + type = MimeTypes.Type.TEXT_HTML; + if (charset == null) + charset = StandardCharsets.ISO_8859_1; + break; + + case "text/json": + case "application/json": + type = MimeTypes.Type.TEXT_JSON; + if (charset == null) + charset = StandardCharsets.UTF_8; + break; + + case "text/plain": + type = MimeTypes.Type.TEXT_PLAIN; + if (charset == null) + charset = StandardCharsets.ISO_8859_1; + break; + + default: + return; + } + + // write into the response aggregate buffer and flush it asynchronously. + while (true) + { + try { - baseRequest.setHandled(true); - Writer writer = getAcceptableWriter(baseRequest, request, response); - if (writer != null) + // TODO currently the writer used here is of fixed size, so a large + // TODO error page may cause a BufferOverflow. In which case we try + // TODO again with stacks disabled. If it still overflows, it is + // TODO written without a body. + ByteBuffer buffer = baseRequest.getResponse().getHttpOutput().acquireBuffer(); + ByteBufferOutputStream out = new ByteBufferOutputStream(buffer); + PrintWriter writer = new PrintWriter(new OutputStreamWriter(out, charset)); + + switch (type) { - response.setContentType(MimeTypes.Type.TEXT_HTML.asString()); - handleErrorPage(request, writer, code, message); + case TEXT_HTML: + response.setContentType(MimeTypes.Type.TEXT_HTML.asString()); + response.setCharacterEncoding(charset.name()); + handleErrorPage(request, writer, code, message); + break; + case TEXT_JSON: + response.setContentType(contentType); + writeErrorJson(request, writer, code, message); + break; + case TEXT_PLAIN: + response.setContentType(MimeTypes.Type.TEXT_PLAIN.asString()); + response.setCharacterEncoding(charset.name()); + writeErrorPlain(request, writer, code, message); + break; + default: + throw new IllegalStateException(); } + + writer.flush(); + break; + } + catch (BufferOverflowException e) + { + LOG.warn("Error page too large: {} {} {}", code, message, request); + if (LOG.isDebugEnabled()) + LOG.warn(e); + baseRequest.getResponse().resetContent(); + if (!_disableStacks) + { + LOG.info("Disabling showsStacks for " + this); + _disableStacks = true; + continue; + } + break; } } + + // Do an asynchronous completion. + baseRequest.getHttpChannel().sendResponseAndComplete(); } protected void handleErrorPage(HttpServletRequest request, Writer writer, int code, String message) @@ -278,12 +343,13 @@ public class ErrorHandler extends AbstractHandler { writer.write("\n"); writer.write("Error "); - writer.write(Integer.toString(code)); - - if (_showMessageInTitle) + // TODO this code is duplicated in writeErrorPageMessage + String status = Integer.toString(code); + writer.write(status); + if (message != null && !message.equals(status)) { writer.write(' '); - write(writer, message); + writer.write(StringUtil.sanitizeXmlString(message)); } writer.write("\n"); } @@ -294,7 +360,7 @@ public class ErrorHandler extends AbstractHandler String uri = request.getRequestURI(); writeErrorPageMessage(request, writer, code, message, uri); - if (showStacks) + if (showStacks && !_disableStacks) writeErrorPageStacks(request, writer); Request.getBaseRequest(request).getHttpChannel().getHttpConfiguration() @@ -305,29 +371,97 @@ public class ErrorHandler extends AbstractHandler throws IOException { writer.write("

HTTP ERROR "); + String status = Integer.toString(code); + writer.write(status); + if (message != null && !message.equals(status)) + { + writer.write(' '); + writer.write(StringUtil.sanitizeXmlString(message)); + } + writer.write("

\n"); + writer.write("\n"); + htmlRow(writer, "URI", uri); + htmlRow(writer, "STATUS", status); + htmlRow(writer, "MESSAGE", message); + htmlRow(writer, "SERVLET", request.getAttribute(Dispatcher.ERROR_SERVLET_NAME)); + Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION); + while (cause != null) + { + htmlRow(writer, "CAUSED BY", cause); + cause = cause.getCause(); + } + writer.write("
\n"); + } + + private void htmlRow(Writer writer, String tag, Object value) + throws IOException + { + writer.write(""); + writer.write(tag); + writer.write(":"); + if (value == null) + writer.write("-"); + else + writer.write(StringUtil.sanitizeXmlString(value.toString())); + writer.write("\n"); + } + + private void writeErrorPlain(HttpServletRequest request, PrintWriter writer, int code, String message) + { + writer.write("HTTP ERROR "); writer.write(Integer.toString(code)); - writer.write("\n

Problem accessing "); - write(writer, uri); - writer.write(". Reason:\n

    ");
-        write(writer, message);
-        writer.write("

"); + writer.write(' '); + writer.write(StringUtil.sanitizeXmlString(message)); + writer.write("\n"); + writer.printf("URI: %s%n", request.getRequestURI()); + writer.printf("STATUS: %s%n", code); + writer.printf("MESSAGE: %s%n", message); + writer.printf("SERVLET: %s%n", request.getAttribute(Dispatcher.ERROR_SERVLET_NAME)); + Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION); + while (cause != null) + { + writer.printf("CAUSED BY %s%n", cause); + if (_showStacks && !_disableStacks) + cause.printStackTrace(writer); + cause = cause.getCause(); + } + } + + private void writeErrorJson(HttpServletRequest request, PrintWriter writer, int code, String message) + { + writer + .append("{\n") + .append(" url: \"").append(request.getRequestURI()).append("\",\n") + .append(" status: \"").append(Integer.toString(code)).append("\",\n") + .append(" message: ").append(QuotedStringTokenizer.quote(message)).append(",\n"); + Object servlet = request.getAttribute(Dispatcher.ERROR_SERVLET_NAME); + if (servlet != null) + writer.append("servlet: \"").append(servlet.toString()).append("\",\n"); + Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION); + int c = 0; + while (cause != null) + { + writer.append(" cause").append(Integer.toString(c++)).append(": ") + .append(QuotedStringTokenizer.quote(cause.toString())).append(",\n"); + cause = cause.getCause(); + } + writer.append("}"); } protected void writeErrorPageStacks(HttpServletRequest request, Writer writer) throws IOException { Throwable th = (Throwable)request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); - while (th != null) + if (_showStacks && th != null) { - writer.write("

Caused by:

");
-            StringWriter sw = new StringWriter();
-            PrintWriter pw = new PrintWriter(sw);
-            th.printStackTrace(pw);
-            pw.flush();
-            write(writer, sw.getBuffer().toString());
+            PrintWriter pw = writer instanceof PrintWriter ? (PrintWriter)writer : new PrintWriter(writer);
+            pw.write("
");
+            while (th != null)
+            {
+                th.printStackTrace(pw);
+                th = th.getCause();
+            }
             writer.write("
\n"); - - th = th.getCause(); } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java index 24a1258b61d..ed5affa51c9 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java @@ -197,8 +197,9 @@ public class ShutdownHandler extends HandlerWrapper connector.shutdown(); } - response.sendError(200, "Connectors closed, commencing full shutdown"); baseRequest.setHandled(true); + response.setStatus(200); + response.flushBuffer(); final Server server = getServer(); new Thread() diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java index ce95a87b203..d554043205c 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java @@ -43,9 +43,7 @@ import static org.hamcrest.Matchers.is; public abstract class AbstractHttpTest { - private static final Set __noBodyCodes = new HashSet<>(Arrays.asList(new String[]{ - "100", "101", "102", "204", "304" - })); + private static final Set __noBodyCodes = new HashSet<>(Arrays.asList("100", "101", "102", "204", "304")); protected static Server server; protected static ServerConnector connector; @@ -87,10 +85,10 @@ public abstract class AbstractHttpTest HttpTester.parseResponse(input, response); if (httpVersion.is("HTTP/1.1") && - response.isComplete() && - response.get("content-length") == null && - response.get("transfer-encoding") == null && - !__noBodyCodes.contains(response.getStatus())) + response.isComplete() && + response.get("content-length") == null && + response.get("transfer-encoding") == null && + !__noBodyCodes.contains(response.getStatus())) assertThat("If HTTP/1.1 response doesn't contain transfer-encoding or content-length headers, " + "it should contain connection:close", response.get("connection"), is("close")); return response; diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java new file mode 100644 index 00000000000..a14a6bcd09f --- /dev/null +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java @@ -0,0 +1,221 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.server; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Exchanger; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; + +import org.eclipse.jetty.http.HttpCompliance; +import org.eclipse.jetty.http.HttpTester; +import org.eclipse.jetty.io.ChannelEndPoint; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.ManagedSelector; +import org.eclipse.jetty.io.SocketChannelEndPoint; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.thread.Scheduler; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.BeforeEach; +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.containsString; +import static org.hamcrest.Matchers.is; + +/** + * Extended Server Tester. + */ +public class AsyncCompletionTest extends HttpServerTestFixture +{ + private static final Exchanger X = new Exchanger<>(); + private static final AtomicBoolean COMPLETE = new AtomicBoolean(); + + private static class DelayedCallback extends Callback.Nested + { + private CompletableFuture _delay = new CompletableFuture<>(); + + public DelayedCallback(Callback callback) + { + super(callback); + } + + @Override + public void succeeded() + { + _delay.complete(null); + } + + @Override + public void failed(Throwable x) + { + _delay.completeExceptionally(x); + } + + public void proceed() + { + try + { + _delay.get(10, TimeUnit.SECONDS); + getCallback().succeeded(); + } + catch(Throwable th) + { + th.printStackTrace(); + getCallback().failed(th); + } + } + } + + + @BeforeEach + public void init() throws Exception + { + COMPLETE.set(false); + + startServer(new ServerConnector(_server, new HttpConnectionFactory() + { + @Override + public Connection newConnection(Connector connector, EndPoint endPoint) + { + return configure(new ExtendedHttpConnection(getHttpConfiguration(), connector, endPoint), connector, endPoint); + } + }) + { + @Override + protected ChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException + { + return new ExtendedEndPoint(channel, selectSet, key, getScheduler()); + } + }); + } + + private static class ExtendedEndPoint extends SocketChannelEndPoint + { + public ExtendedEndPoint(SocketChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler) + { + super(channel, selector, key, scheduler); + } + + @Override + public void write(Callback callback, ByteBuffer... buffers) throws IllegalStateException + { + DelayedCallback delay = new DelayedCallback(callback); + super.write(delay, buffers); + try + { + X.exchange(delay); + } + catch (InterruptedException e) + { + throw new RuntimeException(e); + } + } + } + + private static class ExtendedHttpConnection extends HttpConnection + { + public ExtendedHttpConnection(HttpConfiguration config, Connector connector, EndPoint endPoint) + { + super(config, connector, endPoint, HttpCompliance.RFC7230_LEGACY, false); + } + + @Override + public void onCompleted() + { + COMPLETE.compareAndSet(false,true); + super.onCompleted(); + } + } + + // Tests from here use these parameters + public static Stream tests() + { + List tests = new ArrayList<>(); + tests.add(new Object[]{new HelloWorldHandler(), 200, "Hello world"}); + tests.add(new Object[]{new SendErrorHandler(499,"Test async sendError"), 499, "Test async sendError"}); + return tests.stream().map(Arguments::of); + } + + @ParameterizedTest + @MethodSource("tests") + public void testAsyncCompletion(Handler handler, int status, String message) throws Exception + { + configureServer(handler); + + int base = _threadPool.getBusyThreads(); + try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort())) + { + OutputStream os = client.getOutputStream(); + + // write the request + os.write("GET / HTTP/1.0\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1)); + os.flush(); + + // The write should happen but the callback is delayed + HttpTester.Response response = HttpTester.parseResponse(client.getInputStream()); + assertThat(response, Matchers.notNullValue()); + assertThat(response.getStatus(), is(status)); + String content = response.getContent(); + assertThat(content, containsString(message)); + + // Check that a thread is held busy in write + assertThat(_threadPool.getBusyThreads(), Matchers.greaterThan(base)); + + // Getting the Delayed callback will free the thread + DelayedCallback delay = X.exchange(null, 10, TimeUnit.SECONDS); + + // wait for threads to return to base level + long end = System.nanoTime() + TimeUnit.SECONDS.toNanos(10); + while(_threadPool.getBusyThreads() != base) + { + if (System.nanoTime() > end) + throw new TimeoutException(); + Thread.sleep(10); + } + + // We are now asynchronously waiting! + assertThat(COMPLETE.get(), is(false)); + + // proceed with the completion + delay.proceed(); + + while(!COMPLETE.get()) + { + if (System.nanoTime() > end) + throw new TimeoutException(); + Thread.sleep(10); + } + } + } +} diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ErrorHandlerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ErrorHandlerTest.java index 0f5369036ae..7e396b072b2 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ErrorHandlerTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ErrorHandlerTest.java @@ -30,7 +30,6 @@ import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.server.handler.AbstractHandler; -import org.eclipse.jetty.server.handler.ErrorHandler; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -53,43 +52,6 @@ public class ErrorHandlerTest server = new Server(); connector = new LocalConnector(server); server.addConnector(connector); - server.addBean(new ErrorHandler() - { - @Override - protected void generateAcceptableResponse( - Request baseRequest, - HttpServletRequest request, - HttpServletResponse response, - int code, - String message, - String mimeType) throws IOException - { - switch (mimeType) - { - case "text/json": - case "application/json": - { - baseRequest.setHandled(true); - response.setContentType(mimeType); - response.getWriter() - .append("{") - .append("code: \"").append(Integer.toString(code)).append("\",") - .append("message: \"").append(message).append('"') - .append("}"); - break; - } - case "text/plain": - { - baseRequest.setHandled(true); - response.setContentType("text/plain"); - response.getOutputStream().print(response.getContentType()); - break; - } - default: - super.generateAcceptableResponse(baseRequest, request, response, code, message, mimeType); - } - } - }); server.setHandler(new AbstractHandler() { diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputAsyncStateTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputAsyncStateTest.java index 9c667a9b6c8..fcabf1acfe9 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputAsyncStateTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputAsyncStateTest.java @@ -190,6 +190,10 @@ public class HttpInputAsyncStateTest __history.add("COMPLETE"); break; + case READ_REGISTER: + _state.getHttpChannel().onAsyncWaitForContent(); + break; + default: fail("Bad Action: " + action); } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitBadBehaviourTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitBadBehaviourTest.java deleted file mode 100644 index 336079e4874..00000000000 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitBadBehaviourTest.java +++ /dev/null @@ -1,133 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. -// ------------------------------------------------------------------------ -// All rights reserved. This program and the accompanying materials -// are made available under the terms of the Eclipse Public License v1.0 -// and Apache License v2.0 which accompanies this distribution. -// -// The Eclipse Public License is available at -// http://www.eclipse.org/legal/epl-v10.html -// -// The Apache License v2.0 is available at -// http://www.opensource.org/licenses/apache2.0.php -// -// You may elect to redistribute this code under either of these licenses. -// ======================================================================== -// - -package org.eclipse.jetty.server; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.BrokenBarrierException; -import java.util.concurrent.CyclicBarrier; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.stream.Stream; -import javax.servlet.AsyncContext; -import javax.servlet.DispatcherType; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.eclipse.jetty.http.HttpTester; -import org.eclipse.jetty.http.HttpVersion; -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; - -//TODO: reset buffer tests -//TODO: add protocol specific tests for connection: close and/or chunking - -public class HttpManyWaysToAsyncCommitBadBehaviourTest extends AbstractHttpTest -{ - private final String contextAttribute = getClass().getName() + ".asyncContext"; - - public static Stream httpVersions() - { - // boolean dispatch - if true we dispatch, otherwise we complete - final boolean DISPATCH = true; - final boolean COMPLETE = false; - - List ret = new ArrayList<>(); - ret.add(Arguments.of(HttpVersion.HTTP_1_0, DISPATCH)); - ret.add(Arguments.of(HttpVersion.HTTP_1_0, COMPLETE)); - ret.add(Arguments.of(HttpVersion.HTTP_1_1, DISPATCH)); - ret.add(Arguments.of(HttpVersion.HTTP_1_1, COMPLETE)); - return ret.stream(); - } - - @ParameterizedTest - @MethodSource("httpVersions") - public void testHandlerSetsHandledAndWritesSomeContent(HttpVersion httpVersion, boolean dispatch) throws Exception - { - server.setHandler(new SetHandledWriteSomeDataHandler(false, dispatch)); - server.start(); - - HttpTester.Response response = executeRequest(httpVersion); - - assertThat("response code is 500", response.getStatus(), is(500)); - } - - private class SetHandledWriteSomeDataHandler extends ThrowExceptionOnDemandHandler - { - private final boolean dispatch; - - private SetHandledWriteSomeDataHandler(boolean throwException, boolean dispatch) - { - super(throwException); - this.dispatch = dispatch; - } - - @Override - public void doNonErrorHandle(String target, Request baseRequest, final HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException - { - final CyclicBarrier resumeBarrier = new CyclicBarrier(1); - - if (baseRequest.getDispatcherType() == DispatcherType.ERROR) - { - response.sendError(500); - return; - } - - if (request.getAttribute(contextAttribute) == null) - { - final AsyncContext asyncContext = baseRequest.startAsync(); - new Thread(new Runnable() - { - @Override - public void run() - { - try - { - asyncContext.getResponse().getWriter().write("foobar"); - if (dispatch) - asyncContext.dispatch(); - else - asyncContext.complete(); - resumeBarrier.await(5, TimeUnit.SECONDS); - } - catch (IOException | TimeoutException | InterruptedException | BrokenBarrierException e) - { - e.printStackTrace(); - } - } - }).run(); - } - try - { - resumeBarrier.await(5, TimeUnit.SECONDS); - } - catch (InterruptedException | BrokenBarrierException | TimeoutException e) - { - e.printStackTrace(); - } - throw new TestCommitException(); - } - } -} diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java index ca5ebddc616..82d9b06bc3c 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java @@ -21,6 +21,9 @@ package org.eclipse.jetty.server; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.stream.Stream; import javax.servlet.AsyncContext; import javax.servlet.ServletException; @@ -31,13 +34,16 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.util.log.StacklessLogging; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import static org.eclipse.jetty.http.HttpFieldsMatchers.containsHeaderValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; //TODO: reset buffer tests @@ -51,51 +57,72 @@ public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest // boolean dispatch - if true we dispatch, otherwise we complete final boolean DISPATCH = true; final boolean COMPLETE = false; + final boolean IN_WAIT = true; + final boolean WHILE_DISPATCHED = false; List ret = new ArrayList<>(); - ret.add(Arguments.of(HttpVersion.HTTP_1_0, DISPATCH)); - ret.add(Arguments.of(HttpVersion.HTTP_1_1, DISPATCH)); - ret.add(Arguments.of(HttpVersion.HTTP_1_0, COMPLETE)); - ret.add(Arguments.of(HttpVersion.HTTP_1_1, COMPLETE)); + ret.add(Arguments.of(HttpVersion.HTTP_1_0, DISPATCH, IN_WAIT)); + ret.add(Arguments.of(HttpVersion.HTTP_1_1, DISPATCH, IN_WAIT)); + ret.add(Arguments.of(HttpVersion.HTTP_1_0, COMPLETE, IN_WAIT)); + ret.add(Arguments.of(HttpVersion.HTTP_1_1, COMPLETE, IN_WAIT)); + ret.add(Arguments.of(HttpVersion.HTTP_1_0, DISPATCH, WHILE_DISPATCHED)); + ret.add(Arguments.of(HttpVersion.HTTP_1_1, DISPATCH, WHILE_DISPATCHED)); + ret.add(Arguments.of(HttpVersion.HTTP_1_0, COMPLETE, WHILE_DISPATCHED)); + ret.add(Arguments.of(HttpVersion.HTTP_1_1, COMPLETE, WHILE_DISPATCHED)); return ret.stream(); } @ParameterizedTest @MethodSource("httpVersion") - public void testHandlerDoesNotSetHandled(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testHandlerDoesNotSetHandled(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - DoesNotSetHandledHandler handler = new DoesNotSetHandledHandler(false, dispatch); + DoesNotSetHandledHandler handler = new DoesNotSetHandledHandler(false, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - assertThat("response code", response.getStatus(), is(404)); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(response.getStatus(), is(404)); + assertThat(handler.failure(), is(nullValue())); } @ParameterizedTest @MethodSource("httpVersion") - public void testHandlerDoesNotSetHandledAndThrow(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testHandlerDoesNotSetHandledAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - DoesNotSetHandledHandler handler = new DoesNotSetHandledHandler(true, dispatch); + DoesNotSetHandledHandler handler = new DoesNotSetHandledHandler(true, dispatch, inWait); server.setHandler(handler); server.start(); - HttpTester.Response response = executeRequest(httpVersion); + HttpTester.Response response; + if (inWait) + { + // exception thrown and handled before any async processing + response = executeRequest(httpVersion); + } + else + { + // exception thrown after async processing, so cannot be handled + try (StacklessLogging log = new StacklessLogging(HttpChannelState.class)) + { + response = executeRequest(httpVersion); + } + } - assertThat("response code", response.getStatus(), is(500)); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(response.getStatus(), is(500)); + assertThat(handler.failure(), is(nullValue())); } private class DoesNotSetHandledHandler extends ThrowExceptionOnDemandHandler { private final boolean dispatch; + private final boolean inWait; - private DoesNotSetHandledHandler(boolean throwException, boolean dispatch) + private DoesNotSetHandledHandler(boolean throwException, boolean dispatch, boolean inWait) { super(throwException); this.dispatch = dispatch; + this.inWait = inWait; } @Override @@ -105,17 +132,13 @@ public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest { final AsyncContext asyncContext = baseRequest.startAsync(); request.setAttribute(contextAttribute, asyncContext); - new Thread(new Runnable() + runAsync(baseRequest, inWait, () -> { - @Override - public void run() - { - if (dispatch) - asyncContext.dispatch(); - else - asyncContext.complete(); - } - }).run(); + if (dispatch) + asyncContext.dispatch(); + else + asyncContext.complete(); + }); } super.doNonErrorHandle(target, baseRequest, request, response); } @@ -123,42 +146,57 @@ public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest @ParameterizedTest @MethodSource("httpVersion") - public void testHandlerSetsHandledTrueOnly(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testHandlerSetsHandledTrueOnly(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - OnlySetHandledHandler handler = new OnlySetHandledHandler(false, dispatch); + OnlySetHandledHandler handler = new OnlySetHandledHandler(false, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - assertThat("response code", response.getStatus(), is(200)); + assertThat(response.getStatus(), is(200)); if (httpVersion.is("HTTP/1.1")) assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "0")); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(handler.failure(), is(nullValue())); } @ParameterizedTest @MethodSource("httpVersion") - public void testHandlerSetsHandledTrueOnlyAndThrow(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testHandlerSetsHandledTrueOnlyAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - OnlySetHandledHandler handler = new OnlySetHandledHandler(true, dispatch); + OnlySetHandledHandler handler = new OnlySetHandledHandler(true, dispatch, inWait); server.setHandler(handler); server.start(); - HttpTester.Response response = executeRequest(httpVersion); + HttpTester.Response response; + if (inWait) + { + // exception thrown and handled before any async processing + response = executeRequest(httpVersion); + } + else + { + // exception thrown after async processing, so cannot be handled + try (StacklessLogging log = new StacklessLogging(HttpChannelState.class)) + { + response = executeRequest(httpVersion); + } + } - assertThat("response code", response.getStatus(), is(500)); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(response.getStatus(), is(500)); + assertThat(handler.failure(), is(nullValue())); } private class OnlySetHandledHandler extends ThrowExceptionOnDemandHandler { private final boolean dispatch; + private final boolean inWait; - private OnlySetHandledHandler(boolean throwException, boolean dispatch) + private OnlySetHandledHandler(boolean throwException, boolean dispatch, boolean inWait) { super(throwException); this.dispatch = dispatch; + this.inWait = inWait; } @Override @@ -168,17 +206,13 @@ public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest { final AsyncContext asyncContext = baseRequest.startAsync(); request.setAttribute(contextAttribute, asyncContext); - new Thread(new Runnable() + runAsync(baseRequest, inWait, () -> { - @Override - public void run() - { - if (dispatch) - asyncContext.dispatch(); - else - asyncContext.complete(); - } - }).run(); + if (dispatch) + asyncContext.dispatch(); + else + asyncContext.complete(); + }); } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -187,41 +221,55 @@ public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest @ParameterizedTest @MethodSource("httpVersion") - public void testHandlerSetsHandledAndWritesSomeContent(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testHandlerSetsHandledAndWritesSomeContent(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - SetHandledWriteSomeDataHandler handler = new SetHandledWriteSomeDataHandler(false, dispatch); + SetHandledWriteSomeDataHandler handler = new SetHandledWriteSomeDataHandler(false, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - assertThat("response code", response.getStatus(), is(200)); + assertThat(response.getStatus(), is(200)); assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "6")); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(handler.failure(), is(nullValue())); } @ParameterizedTest @MethodSource("httpVersion") - public void testHandlerSetsHandledAndWritesSomeContentAndThrow(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testHandlerSetsHandledAndWritesSomeContentAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - SetHandledWriteSomeDataHandler handler = new SetHandledWriteSomeDataHandler(true, dispatch); + SetHandledWriteSomeDataHandler handler = new SetHandledWriteSomeDataHandler(true, dispatch, inWait); server.setHandler(handler); server.start(); + HttpTester.Response response; + if (inWait) + { + // exception thrown and handled before any async processing + response = executeRequest(httpVersion); + } + else + { + // exception thrown after async processing, so cannot be handled + try (StacklessLogging log = new StacklessLogging(HttpChannelState.class)) + { + response = executeRequest(httpVersion); + } + } - HttpTester.Response response = executeRequest(httpVersion); - - assertThat("response code", response.getStatus(), is(500)); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(response.getStatus(), is(500)); + assertThat(handler.failure(), is(nullValue())); } private class SetHandledWriteSomeDataHandler extends ThrowExceptionOnDemandHandler { private final boolean dispatch; + private final boolean inWait; - private SetHandledWriteSomeDataHandler(boolean throwException, boolean dispatch) + private SetHandledWriteSomeDataHandler(boolean throwException, boolean dispatch, boolean inWait) { super(throwException); this.dispatch = dispatch; + this.inWait = inWait; } @Override @@ -231,25 +279,21 @@ public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest { final AsyncContext asyncContext = baseRequest.startAsync(); request.setAttribute(contextAttribute, asyncContext); - new Thread(new Runnable() + runAsync(baseRequest, inWait, () -> { - @Override - public void run() + try { - try - { - asyncContext.getResponse().getWriter().write("foobar"); - if (dispatch) - asyncContext.dispatch(); - else - asyncContext.complete(); - } - catch (IOException e) - { - markFailed(e); - } + asyncContext.getResponse().getWriter().write("foobar"); + if (dispatch) + asyncContext.dispatch(); + else + asyncContext.complete(); } - }).run(); + catch (IOException e) + { + markFailed(e); + } + }); } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -258,44 +302,55 @@ public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest @ParameterizedTest @MethodSource("httpVersion") - public void testHandlerExplicitFlush(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testHandlerExplicitFlush(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - ExplicitFlushHandler handler = new ExplicitFlushHandler(false, dispatch); + ExplicitFlushHandler handler = new ExplicitFlushHandler(false, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - assertThat("response code", response.getStatus(), is(200)); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(response.getStatus(), is(200)); + assertThat(handler.failure(), is(nullValue())); if (httpVersion.is("HTTP/1.1")) assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked")); } @ParameterizedTest @MethodSource("httpVersion") - public void testHandlerExplicitFlushAndThrow(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testHandlerExplicitFlushAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - ExplicitFlushHandler handler = new ExplicitFlushHandler(true, dispatch); + ExplicitFlushHandler handler = new ExplicitFlushHandler(true, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - assertThat("response code", response.getStatus(), is(200)); - assertThat("no exceptions", handler.failure(), is(nullValue())); - if (httpVersion.is("HTTP/1.1")) - assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked")); + if (inWait) + { + // throw happens before flush + assertThat(response.getStatus(), is(500)); + } + else + { + // flush happens before throw + assertThat(response.getStatus(), is(200)); + if (httpVersion.is("HTTP/1.1")) + assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked")); + } + assertThat(handler.failure(), is(nullValue())); } private class ExplicitFlushHandler extends ThrowExceptionOnDemandHandler { private final boolean dispatch; + private final boolean inWait; - private ExplicitFlushHandler(boolean throwException, boolean dispatch) + private ExplicitFlushHandler(boolean throwException, boolean dispatch, boolean inWait) { super(throwException); this.dispatch = dispatch; + this.inWait = inWait; } @Override @@ -305,27 +360,23 @@ public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest { final AsyncContext asyncContext = baseRequest.startAsync(); request.setAttribute(contextAttribute, asyncContext); - new Thread(new Runnable() + runAsync(baseRequest, inWait, () -> { - @Override - public void run() + try { - try - { - ServletResponse asyncContextResponse = asyncContext.getResponse(); - asyncContextResponse.getWriter().write("foobar"); - asyncContextResponse.flushBuffer(); - if (dispatch) - asyncContext.dispatch(); - else - asyncContext.complete(); - } - catch (IOException e) - { - markFailed(e); - } + ServletResponse asyncContextResponse = asyncContext.getResponse(); + asyncContextResponse.getWriter().write("foobar"); + asyncContextResponse.flushBuffer(); + if (dispatch) + asyncContext.dispatch(); + else + asyncContext.complete(); } - }).run(); + catch (IOException e) + { + markFailed(e); + } + }); } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -334,44 +385,55 @@ public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest @ParameterizedTest @MethodSource("httpVersion") - public void testHandledAndFlushWithoutContent(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testHandledAndFlushWithoutContent(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - SetHandledAndFlushWithoutContentHandler handler = new SetHandledAndFlushWithoutContentHandler(false, dispatch); + SetHandledAndFlushWithoutContentHandler handler = new SetHandledAndFlushWithoutContentHandler(false, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - assertThat("response code", response.getStatus(), is(200)); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(response.getStatus(), is(200)); + assertThat(handler.failure(), is(nullValue())); if (httpVersion.is("HTTP/1.1")) assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked")); } @ParameterizedTest @MethodSource("httpVersion") - public void testHandledAndFlushWithoutContentAndThrow(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testHandledAndFlushWithoutContentAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - SetHandledAndFlushWithoutContentHandler handler = new SetHandledAndFlushWithoutContentHandler(true, dispatch); + SetHandledAndFlushWithoutContentHandler handler = new SetHandledAndFlushWithoutContentHandler(true, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - assertThat("response code", response.getStatus(), is(200)); - assertThat("no exceptions", handler.failure(), is(nullValue())); - if (httpVersion.is("HTTP/1.1")) - assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked")); + if (inWait) + { + // throw happens before async behaviour, so is handled + assertThat(response.getStatus(), is(500)); + } + else + { + assertThat(response.getStatus(), is(200)); + if (httpVersion.is("HTTP/1.1")) + assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked")); + } + + assertThat(handler.failure(), is(nullValue())); } private class SetHandledAndFlushWithoutContentHandler extends ThrowExceptionOnDemandHandler { private final boolean dispatch; + private final boolean inWait; - private SetHandledAndFlushWithoutContentHandler(boolean throwException, boolean dispatch) + private SetHandledAndFlushWithoutContentHandler(boolean throwException, boolean dispatch, boolean inWait) { super(throwException); this.dispatch = dispatch; + this.inWait = inWait; } @Override @@ -381,25 +443,21 @@ public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest { final AsyncContext asyncContext = baseRequest.startAsync(); request.setAttribute(contextAttribute, asyncContext); - new Thread(new Runnable() + runAsync(baseRequest, inWait, () -> { - @Override - public void run() + try { - try - { - asyncContext.getResponse().flushBuffer(); - if (dispatch) - asyncContext.dispatch(); - else - asyncContext.complete(); - } - catch (IOException e) - { - markFailed(e); - } + asyncContext.getResponse().flushBuffer(); + if (dispatch) + asyncContext.dispatch(); + else + asyncContext.complete(); } - }).run(); + catch (IOException e) + { + markFailed(e); + } + }); } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -408,16 +466,16 @@ public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest @ParameterizedTest @MethodSource("httpVersion") - public void testWriteFlushWriteMore(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testWriteFlushWriteMore(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - WriteFlushWriteMoreHandler handler = new WriteFlushWriteMoreHandler(false, dispatch); + WriteFlushWriteMoreHandler handler = new WriteFlushWriteMoreHandler(false, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - assertThat("response code", response.getStatus(), is(200)); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(response.getStatus(), is(200)); + assertThat(handler.failure(), is(nullValue())); // HTTP/1.0 does not do chunked. it will just send content and close if (httpVersion.is("HTTP/1.1")) @@ -426,28 +484,39 @@ public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest @ParameterizedTest @MethodSource("httpVersion") - public void testWriteFlushWriteMoreAndThrow(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testWriteFlushWriteMoreAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - WriteFlushWriteMoreHandler handler = new WriteFlushWriteMoreHandler(true, dispatch); + WriteFlushWriteMoreHandler handler = new WriteFlushWriteMoreHandler(true, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - assertThat("response code", response.getStatus(), is(200)); - assertThat("no exceptions", handler.failure(), is(nullValue())); - if (httpVersion.is("HTTP/1.1")) - assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked")); + if (inWait) + { + // The exception is thrown before we do any writing or async operations, so it delivered as onError and then + // dispatched. + assertThat(response.getStatus(), is(500)); + } + else + { + assertThat(response.getStatus(), is(200)); + if (httpVersion.is("HTTP/1.1")) + assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked")); + } + assertThat(handler.failure(), is(nullValue())); } private class WriteFlushWriteMoreHandler extends ThrowExceptionOnDemandHandler { private final boolean dispatch; + private final boolean inWait; - private WriteFlushWriteMoreHandler(boolean throwException, boolean dispatch) + private WriteFlushWriteMoreHandler(boolean throwException, boolean dispatch, boolean inWait) { super(throwException); this.dispatch = dispatch; + this.inWait = inWait; } @Override @@ -457,28 +526,24 @@ public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest { final AsyncContext asyncContext = baseRequest.startAsync(); request.setAttribute(contextAttribute, asyncContext); - new Thread(new Runnable() + runAsync(baseRequest, inWait, () -> { - @Override - public void run() + try { - try - { - ServletResponse asyncContextResponse = asyncContext.getResponse(); - asyncContextResponse.getWriter().write("foo"); - asyncContextResponse.flushBuffer(); - asyncContextResponse.getWriter().write("bar"); - if (dispatch) - asyncContext.dispatch(); - else - asyncContext.complete(); - } - catch (IOException e) - { - markFailed(e); - } + ServletResponse asyncContextResponse = asyncContext.getResponse(); + asyncContextResponse.getWriter().write("foo"); + asyncContextResponse.flushBuffer(); + asyncContextResponse.getWriter().write("bar"); + if (dispatch) + asyncContext.dispatch(); + else + asyncContext.complete(); } - }).run(); + catch (IOException e) + { + markFailed(e); + } + }); } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -487,47 +552,58 @@ public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest @ParameterizedTest @MethodSource("httpVersion") - public void testBufferOverflow(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testBufferOverflow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - OverflowHandler handler = new OverflowHandler(false, dispatch); + OverflowHandler handler = new OverflowHandler(false, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - assertThat("response code", response.getStatus(), is(200)); + assertThat(response.getStatus(), is(200)); assertThat(response.getContent(), is("foobar")); if (httpVersion.is("HTTP/1.1")) assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked")); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(handler.failure(), is(nullValue())); } @ParameterizedTest @MethodSource("httpVersion") - public void testBufferOverflowAndThrow(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testBufferOverflowAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - OverflowHandler handler = new OverflowHandler(true, dispatch); + OverflowHandler handler = new OverflowHandler(true, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - // Buffer size is too small, so the content is written directly producing a 200 response - assertThat("response code", response.getStatus(), is(200)); - assertThat(response.getContent(), is("foobar")); - if (httpVersion.is("HTTP/1.1")) - assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked")); - assertThat("no exceptions", handler.failure(), is(nullValue())); + // Buffer size smaller than content, so writing will commit response. + // If this happens before the exception is thrown we get a 200, else a 500 is produced + if (inWait) + { + assertThat(response.getStatus(), is(500)); + assertThat(response.getContent(), containsString("TestCommitException: Thrown by test")); + } + else + { + assertThat(response.getStatus(), is(200)); + assertThat(response.getContent(), is("foobar")); + if (httpVersion.is("HTTP/1.1")) + assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked")); + assertThat(handler.failure(), is(nullValue())); + } } private class OverflowHandler extends ThrowExceptionOnDemandHandler { private final boolean dispatch; + private final boolean inWait; - private OverflowHandler(boolean throwException, boolean dispatch) + private OverflowHandler(boolean throwException, boolean dispatch, boolean inWait) { super(throwException); this.dispatch = dispatch; + this.inWait = inWait; } @Override @@ -537,27 +613,23 @@ public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest { final AsyncContext asyncContext = baseRequest.startAsync(); request.setAttribute(contextAttribute, asyncContext); - new Thread(new Runnable() + runAsync(baseRequest, inWait, () -> { - @Override - public void run() + try { - try - { - ServletResponse asyncContextResponse = asyncContext.getResponse(); - asyncContextResponse.setBufferSize(3); - asyncContextResponse.getWriter().write("foobar"); - if (dispatch) - asyncContext.dispatch(); - else - asyncContext.complete(); - } - catch (IOException e) - { - markFailed(e); - } + ServletResponse asyncContextResponse = asyncContext.getResponse(); + asyncContextResponse.setBufferSize(3); + asyncContextResponse.getWriter().write("foobar"); + if (dispatch) + asyncContext.dispatch(); + else + asyncContext.complete(); } - }).run(); + catch (IOException e) + { + markFailed(e); + } + }); } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -566,45 +638,54 @@ public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest @ParameterizedTest @MethodSource("httpVersion") - public void testSetContentLengthAndWriteExactlyThatAmountOfBytes(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testSetContentLengthAndWriteExactlyThatAmountOfBytes(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - SetContentLengthAndWriteThatAmountOfBytesHandler handler = new SetContentLengthAndWriteThatAmountOfBytesHandler(false, dispatch); + SetContentLengthAndWriteThatAmountOfBytesHandler handler = new SetContentLengthAndWriteThatAmountOfBytesHandler(false, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - assertThat("response code", response.getStatus(), is(200)); - assertThat("response body", response.getContent(), is("foo")); + assertThat(response.getStatus(), is(200)); + assertThat(response.getContent(), is("foo")); assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "3")); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(handler.failure(), is(nullValue())); } @ParameterizedTest @MethodSource("httpVersion") - public void testSetContentLengthAndWriteExactlyThatAmountOfBytesAndThrow(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testSetContentLengthAndWriteExactlyThatAmountOfBytesAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - SetContentLengthAndWriteThatAmountOfBytesHandler handler = new SetContentLengthAndWriteThatAmountOfBytesHandler(true, dispatch); + SetContentLengthAndWriteThatAmountOfBytesHandler handler = new SetContentLengthAndWriteThatAmountOfBytesHandler(true, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - //TODO: should we expect 500 here? - assertThat("response code", response.getStatus(), is(200)); - assertThat("response body", response.getContent(), is("foo")); - assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "3")); - assertThat("no exceptions", handler.failure(), is(nullValue())); + if (inWait) + { + // too late! + assertThat(response.getStatus(), is(500)); + } + else + { + assertThat(response.getStatus(), is(200)); + assertThat(response.getContent(), is("foo")); + assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "3")); + } + assertThat(handler.failure(), is(nullValue())); } private class SetContentLengthAndWriteThatAmountOfBytesHandler extends ThrowExceptionOnDemandHandler { private final boolean dispatch; + private final boolean inWait; - private SetContentLengthAndWriteThatAmountOfBytesHandler(boolean throwException, boolean dispatch) + private SetContentLengthAndWriteThatAmountOfBytesHandler(boolean throwException, boolean dispatch, boolean inWait) { super(throwException); this.dispatch = dispatch; + this.inWait = inWait; } @Override @@ -614,27 +695,23 @@ public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest { final AsyncContext asyncContext = baseRequest.startAsync(); request.setAttribute(contextAttribute, asyncContext); - new Thread(new Runnable() + runAsync(baseRequest, inWait, () -> { - @Override - public void run() + try { - try - { - ServletResponse asyncContextResponse = asyncContext.getResponse(); - asyncContextResponse.setContentLength(3); - asyncContextResponse.getWriter().write("foo"); - if (dispatch) - asyncContext.dispatch(); - else - asyncContext.complete(); - } - catch (IOException e) - { - markFailed(e); - } + ServletResponse asyncContextResponse = asyncContext.getResponse(); + asyncContextResponse.setContentLength(3); + asyncContextResponse.getWriter().write("foo"); + if (dispatch) + asyncContext.dispatch(); + else + asyncContext.complete(); } - }).run(); + catch (IOException e) + { + markFailed(e); + } + }); } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -643,46 +720,55 @@ public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest @ParameterizedTest @MethodSource("httpVersion") - public void testSetContentLengthAndWriteMoreBytes(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testSetContentLengthAndWriteMoreBytes(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - SetContentLengthAndWriteMoreBytesHandler handler = new SetContentLengthAndWriteMoreBytesHandler(false, dispatch); + SetContentLengthAndWriteMoreBytesHandler handler = new SetContentLengthAndWriteMoreBytesHandler(false, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - assertThat("response code", response.getStatus(), is(200)); + assertThat(response.getStatus(), is(200)); // jetty truncates the body when content-length is reached.! This is correct and desired behaviour? - assertThat("response body", response.getContent(), is("foo")); + assertThat(response.getContent(), is("foo")); assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "3")); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(handler.failure(), is(nullValue())); } @ParameterizedTest @MethodSource("httpVersion") - public void testSetContentLengthAndWriteMoreAndThrow(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testSetContentLengthAndWriteMoreAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - SetContentLengthAndWriteMoreBytesHandler handler = new SetContentLengthAndWriteMoreBytesHandler(true, dispatch); + SetContentLengthAndWriteMoreBytesHandler handler = new SetContentLengthAndWriteMoreBytesHandler(true, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - // TODO: we throw before response is committed. should we expect 500? - assertThat("response code", response.getStatus(), is(200)); - assertThat("response body", response.getContent(), is("foo")); - assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "3")); - assertThat("no exceptions", handler.failure(), is(nullValue())); + if (inWait) + { + // too late! + assertThat(response.getStatus(), is(500)); + } + else + { + assertThat(response.getStatus(), is(200)); + assertThat(response.getContent(), is("foo")); + assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "3")); + } + assertThat(handler.failure(), is(nullValue())); } private class SetContentLengthAndWriteMoreBytesHandler extends ThrowExceptionOnDemandHandler { private final boolean dispatch; + private final boolean inWait; - private SetContentLengthAndWriteMoreBytesHandler(boolean throwException, boolean dispatch) + private SetContentLengthAndWriteMoreBytesHandler(boolean throwException, boolean dispatch, boolean inWait) { super(throwException); this.dispatch = dispatch; + this.inWait = inWait; } @Override @@ -692,27 +778,23 @@ public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest { final AsyncContext asyncContext = baseRequest.startAsync(); request.setAttribute(contextAttribute, asyncContext); - new Thread(new Runnable() + runAsync(baseRequest, inWait, () -> { - @Override - public void run() + try { - try - { - ServletResponse asyncContextResponse = asyncContext.getResponse(); - asyncContextResponse.setContentLength(3); - asyncContextResponse.getWriter().write("foobar"); - if (dispatch) - asyncContext.dispatch(); - else - asyncContext.complete(); - } - catch (IOException e) - { - markFailed(e); - } + ServletResponse asyncContextResponse = asyncContext.getResponse(); + asyncContextResponse.setContentLength(3); + asyncContextResponse.getWriter().write("foobar"); + if (dispatch) + asyncContext.dispatch(); + else + asyncContext.complete(); } - }).run(); + catch (IOException e) + { + markFailed(e); + } + }); } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -721,41 +803,50 @@ public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest @ParameterizedTest @MethodSource("httpVersion") - public void testWriteAndSetContentLength(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testWriteAndSetContentLength(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - WriteAndSetContentLengthHandler handler = new WriteAndSetContentLengthHandler(false, dispatch); + WriteAndSetContentLengthHandler handler = new WriteAndSetContentLengthHandler(false, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - assertThat("response code", response.getStatus(), is(200)); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(response.getStatus(), is(200)); + assertThat(handler.failure(), is(nullValue())); //TODO: jetty ignores setContentLength and sends transfer-encoding header. Correct? } @ParameterizedTest @MethodSource("httpVersion") - public void testWriteAndSetContentLengthAndThrow(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testWriteAndSetContentLengthAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - WriteAndSetContentLengthHandler handler = new WriteAndSetContentLengthHandler(true, dispatch); + WriteAndSetContentLengthHandler handler = new WriteAndSetContentLengthHandler(true, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - - assertThat("response code", response.getStatus(), is(200)); - assertThat("no exceptions", handler.failure(), is(nullValue())); + if (inWait) + { + // too late + assertThat(response.getStatus(), is(500)); + } + else + { + assertThat(response.getStatus(), is(200)); + } + assertThat(handler.failure(), is(nullValue())); } private class WriteAndSetContentLengthHandler extends ThrowExceptionOnDemandHandler { private final boolean dispatch; + private final boolean inWait; - private WriteAndSetContentLengthHandler(boolean throwException, boolean dispatch) + private WriteAndSetContentLengthHandler(boolean throwException, boolean dispatch, boolean inWait) { super(throwException); this.dispatch = dispatch; + this.inWait = inWait; } @Override @@ -765,27 +856,23 @@ public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest { final AsyncContext asyncContext = baseRequest.startAsync(); request.setAttribute(contextAttribute, asyncContext); - new Thread(new Runnable() + runAsync(baseRequest, inWait, () -> { - @Override - public void run() + try { - try - { - ServletResponse asyncContextResponse = asyncContext.getResponse(); - asyncContextResponse.getWriter().write("foo"); - asyncContextResponse.setContentLength(3); // This should commit the response - if (dispatch) - asyncContext.dispatch(); - else - asyncContext.complete(); - } - catch (IOException e) - { - markFailed(e); - } + ServletResponse asyncContextResponse = asyncContext.getResponse(); + asyncContextResponse.getWriter().write("foo"); + asyncContextResponse.setContentLength(3); // This should commit the response + if (dispatch) + asyncContext.dispatch(); + else + asyncContext.complete(); } - }).run(); + catch (IOException e) + { + markFailed(e); + } + }); } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -794,42 +881,52 @@ public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest @ParameterizedTest @MethodSource("httpVersion") - public void testWriteAndSetContentLengthTooSmall(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testWriteAndSetContentLengthTooSmall(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - WriteAndSetContentLengthTooSmallHandler handler = new WriteAndSetContentLengthTooSmallHandler(false, dispatch); + WriteAndSetContentLengthTooSmallHandler handler = new WriteAndSetContentLengthTooSmallHandler(false, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - // Setting a content-length too small throws an IllegalStateException - assertThat("response code", response.getStatus(), is(500)); - assertThat("no exceptions", handler.failure(), is(nullValue())); + // Setting a content-length too small throws an IllegalStateException, + // but only in the async handler, which completes or dispatches anyway + assertThat(response.getStatus(), is(200)); + assertThat(handler.failure(), not(is(nullValue()))); } @ParameterizedTest @MethodSource("httpVersion") - public void testWriteAndSetContentLengthTooSmallAndThrow(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testWriteAndSetContentLengthTooSmallAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - WriteAndSetContentLengthTooSmallHandler handler = new WriteAndSetContentLengthTooSmallHandler(true, dispatch); + WriteAndSetContentLengthTooSmallHandler handler = new WriteAndSetContentLengthTooSmallHandler(true, dispatch, inWait); server.setHandler(handler); server.start(); - HttpTester.Response response = executeRequest(httpVersion); + HttpTester.Response response; + try (StacklessLogging stackless = new StacklessLogging(HttpChannelState.class)) + { + response = executeRequest(httpVersion); + } - // Setting a content-length too small throws an IllegalStateException - assertThat("response code", response.getStatus(), is(500)); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(response.getStatus(), is(500)); + + if (!inWait) + assertThat(handler.failure(), not(is(nullValue()))); + else + assertThat(handler.failure(), is(nullValue())); } private class WriteAndSetContentLengthTooSmallHandler extends ThrowExceptionOnDemandHandler { private final boolean dispatch; + private final boolean inWait; - private WriteAndSetContentLengthTooSmallHandler(boolean throwException, boolean dispatch) + private WriteAndSetContentLengthTooSmallHandler(boolean throwException, boolean dispatch, boolean inWait) { super(throwException); this.dispatch = dispatch; + this.inWait = inWait; } @Override @@ -839,30 +936,93 @@ public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest { final AsyncContext asyncContext = baseRequest.startAsync(); request.setAttribute(contextAttribute, asyncContext); - new Thread(new Runnable() + runAsync(baseRequest, inWait, () -> { - @Override - public void run() + try { - try - { - ServletResponse asyncContextResponse = asyncContext.getResponse(); - asyncContextResponse.getWriter().write("foobar"); - asyncContextResponse.setContentLength(3); - if (dispatch) - asyncContext.dispatch(); - else - asyncContext.complete(); - } - catch (IOException e) - { - markFailed(e); - } + ServletResponse asyncContextResponse = asyncContext.getResponse(); + asyncContextResponse.getWriter().write("foobar"); + asyncContextResponse.setContentLength(3); } - }).run(); + catch (Throwable e) + { + markFailed(e); + if (dispatch) + asyncContext.dispatch(); + else + asyncContext.complete(); + } + }); } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); } } + + private void runAsyncInAsyncWait(Request request, Runnable task) + { + server.getThreadPool().execute(() -> + { + long end = System.nanoTime() + TimeUnit.SECONDS.toNanos(10); + try + { + while (System.nanoTime() < end) + { + switch (request.getHttpChannelState().getState()) + { + case WAITING: + task.run(); + return; + + case HANDLING: + Thread.sleep(100); + continue; + + default: + request.getHttpChannel().abort(new IllegalStateException()); + return; + } + } + request.getHttpChannel().abort(new TimeoutException()); + } + catch (InterruptedException e) + { + e.printStackTrace(); + } + }); + } + + private void runAsyncWhileDispatched(Runnable task) + { + CountDownLatch ran = new CountDownLatch(1); + + server.getThreadPool().execute(() -> + { + try + { + task.run(); + } + finally + { + ran.countDown(); + } + }); + + try + { + ran.await(10, TimeUnit.SECONDS); + } + catch (InterruptedException e) + { + throw new RuntimeException(e); + } + } + + private void runAsync(Request request, boolean inWait, Runnable task) + { + if (inWait) + runAsyncInAsyncWait(request, task); + else + runAsyncWhileDispatched(task); + } } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java index 3102db8873d..fd687f3b7a5 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java @@ -65,13 +65,22 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public abstract class HttpServerTestBase extends HttpServerTestFixture { - private static final String REQUEST1_HEADER = "POST / HTTP/1.0\n" + "Host: localhost\n" + "Content-Type: text/xml; charset=utf-8\n" + "Connection: close\n" + "Content-Length: "; + private static final String REQUEST1_HEADER = "POST / HTTP/1.0\n" + + "Host: localhost\n" + + "Content-Type: text/xml; charset=utf-8\n" + + "Connection: close\n" + + "Content-Length: "; private static final String REQUEST1_CONTENT = "\n" + - "\n" + - ""; + "\n" + + ""; private static final String REQUEST1 = REQUEST1_HEADER + REQUEST1_CONTENT.getBytes().length + "\n\n" + REQUEST1_CONTENT; - private static final String RESPONSE1 = "HTTP/1.1 200 OK\n" + "Content-Length: 13\n" + "Server: Jetty(" + Server.getVersion() + ")\n" + "\n" + "Hello world\n"; + private static final String RESPONSE1 = "HTTP/1.1 200 OK\n" + + "Content-Length: 13\n" + + "Server: Jetty(" + Server.getVersion() + ")\n" + + "\n" + + "Hello world\n"; // Break the request up into three pieces, splitting the header. private static final String FRAGMENT1 = REQUEST1.substring(0, 16); @@ -104,7 +113,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture " 73\n" + " \n" + " \n" + - "\n"; + "\n"; protected static final String RESPONSE2 = "HTTP/1.1 200 OK\n" + "Content-Type: text/xml;charset=iso-8859-1\n" + @@ -143,9 +152,9 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture OutputStream os = client.getOutputStream(); os.write(("OPTIONS * HTTP/1.1\r\n" + - "Host: " + _serverURI.getHost() + "\r\n" + - "Connection: close\r\n" + - "\r\n").getBytes(StandardCharsets.ISO_8859_1)); + "Host: " + _serverURI.getHost() + "\r\n" + + "Connection: close\r\n" + + "\r\n").getBytes(StandardCharsets.ISO_8859_1)); os.flush(); // Read the response. @@ -154,15 +163,20 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture assertThat(response, Matchers.containsString("HTTP/1.1 200 OK")); assertThat(response, Matchers.containsString("Allow: GET")); } + } + @Test + public void testGETStar() throws Exception + { + configureServer(new OptionsHandler()); try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort())) { OutputStream os = client.getOutputStream(); os.write(("GET * HTTP/1.1\r\n" + - "Host: " + _serverURI.getHost() + "\r\n" + - "Connection: close\r\n" + - "\r\n").getBytes(StandardCharsets.ISO_8859_1)); + "Host: " + _serverURI.getHost() + "\r\n" + + "Connection: close\r\n" + + "\r\n").getBytes(StandardCharsets.ISO_8859_1)); os.flush(); // Read the response. @@ -434,27 +448,22 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort())) { OutputStream os = client.getOutputStream(); - //@checkstyle-disable-check : IllegalTokenText - os.write(("GET /R2 HTTP/1.1\015\012" + - "Host: localhost\015\012" + - "Transfer-Encoding: chunked\015\012" + - "Content-Type: text/plain\015\012" + - "Connection: close\015\012" + - "\015\012").getBytes()); - //@checkstyle-enable-check : IllegalTokenText + + os.write(("GET /R2 HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Transfer-Encoding: chunked\r\n" + + "Content-Type: text/plain\r\n" + + "Connection: close\r\n" + + "\r\n").getBytes()); os.flush(); Thread.sleep(1000); os.write(("5").getBytes()); Thread.sleep(1000); - //@checkstyle-disable-check : IllegalTokenText - os.write(("\015\012").getBytes()); - //@checkstyle-enable-check : IllegalTokenText + os.write(("\r\n").getBytes()); os.flush(); Thread.sleep(1000); - //@checkstyle-disable-check : IllegalTokenText - os.write(("ABCDE\015\012" + - "0;\015\012\015\012").getBytes()); - //@checkstyle-enable-check : IllegalTokenText + os.write(("ABCDE\r\n" + + "0;\r\n\r\n").getBytes()); os.flush(); // Read the response. @@ -472,14 +481,14 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture { OutputStream os = client.getOutputStream(); //@checkstyle-disable-check : IllegalTokenText - os.write(("GET /R2 HTTP/1.1\015\012" + - "Host: localhost\015\012" + - "Content-Length: 5\015\012" + - "Content-Type: text/plain\015\012" + - "Connection: close\015\012" + - "\015\012" + - "ABCDE\015\012" + - "\015\012" + os.write(("GET /R2 HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Content-Length: 5\r\n" + + "Content-Type: text/plain\r\n" + + "Connection: close\r\n" + + "\r\n" + + "ABCDE\r\n" + + "\r\n" //@checkstyle-enable-check : IllegalTokenText ).getBytes()); os.flush(); @@ -1136,26 +1145,26 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture //@checkstyle-disable-check : IllegalTokenText os.write(( - "POST /R1 HTTP/1.1\015\012" + + "POST /R1 HTTP/1.1\r\n" + "Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + "content-type: text/plain; charset=utf-8\r\n" + "content-length: 10\r\n" + - "\015\012" + + "\r\n" + "123456789\n" + - "HEAD /R2 HTTP/1.1\015\012" + - "Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\015\012" + + "HEAD /R2 HTTP/1.1\r\n" + + "Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + "content-type: text/plain; charset=utf-8\r\n" + "content-length: 10\r\n" + - "\015\012" + + "\r\n" + "ABCDEFGHI\n" + - "POST /R3 HTTP/1.1\015\012" + - "Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\015\012" + + "POST /R3 HTTP/1.1\r\n" + + "Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + "content-type: text/plain; charset=utf-8\r\n" + "content-length: 10\r\n" + - "Connection: close\015\012" + - "\015\012" + + "Connection: close\r\n" + + "\r\n" + "abcdefghi\n" //@checkstyle-enable-check : IllegalTokenText ).getBytes(StandardCharsets.ISO_8859_1)); @@ -1553,12 +1562,11 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture { try { - byte[] bytes = ( - "GET / HTTP/1.1\r\n" + - "Host: localhost\r\n" + - "Content-Length: " + cl + "\r\n" + - "\r\n" + - content).getBytes(StandardCharsets.ISO_8859_1); + byte[] bytes = ("GET / HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Content-Length: " + cl + "\r\n" + + "\r\n" + + content).getBytes(StandardCharsets.ISO_8859_1); for (int i = 0; i < REQS; i++) { diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestFixture.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestFixture.java index 96a17208dd5..bd009b30d24 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestFixture.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestFixture.java @@ -40,7 +40,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; public class HttpServerTestFixture -{ // Useful constants +{ + // Useful constants protected static final long PAUSE = 10L; protected static final int LOOPS = 50; @@ -186,6 +187,31 @@ public class HttpServerTestFixture } } + + protected static class SendErrorHandler extends AbstractHandler + { + private final int code; + private final String message; + + public SendErrorHandler() + { + this(500, null); + } + + public SendErrorHandler(int code, String message) + { + this.code = code; + this.message = message; + } + + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + baseRequest.setHandled(true); + response.sendError(code, message); + } + } + protected static class ReadExactHandler extends AbstractHandler.ErrorDispatchHandler { private int expected; diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/LocalAsyncContextTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/LocalAsyncContextTest.java index 81c89ae2331..001b7438437 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/LocalAsyncContextTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/LocalAsyncContextTest.java @@ -32,6 +32,8 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.server.handler.HandlerWrapper; import org.eclipse.jetty.server.session.SessionHandler; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -42,6 +44,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; public class LocalAsyncContextTest { + public static final Logger LOG = Log.getLogger(LocalAsyncContextTest.class); protected Server _server; protected SuspendHandler _handler; protected Connector _connector; @@ -232,6 +235,7 @@ public class LocalAsyncContextTest private synchronized String process(String content) throws Exception { + LOG.debug("TEST process: {}", content); reset(); String request = "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + @@ -305,6 +309,7 @@ public class LocalAsyncContextTest @Override public void handle(String target, final Request baseRequest, final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException { + LOG.debug("handle {} {}", baseRequest.getDispatcherType(), baseRequest); if (DispatcherType.REQUEST.equals(baseRequest.getDispatcherType())) { if (_read > 0) diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java index a872ebdc824..4cb4f63a48a 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java @@ -35,6 +35,8 @@ import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.Locale; +import java.util.stream.Stream; +import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; import javax.servlet.http.Cookie; @@ -70,6 +72,8 @@ import org.hamcrest.Matchers; 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.MethodSource; import static java.nio.charset.StandardCharsets.UTF_8; import static org.hamcrest.MatcherAssert.assertThat; @@ -664,65 +668,41 @@ public class ResponseTest assertEquals("foo/bar; other=pq charset=utf-8 other=xyz;charset=utf-16", response.getContentType()); } - @Test - public void testStatusCodes() throws Exception + public static Stream sendErrorTestCodes() { - Response response = getResponse(); - - response.sendError(404); - assertEquals(404, response.getStatus()); - assertEquals("Not Found", response.getReason()); - - response = getResponse(); - - response.sendError(500, "Database Error"); - assertEquals(500, response.getStatus()); - assertEquals("Database Error", response.getReason()); - assertEquals("must-revalidate,no-cache,no-store", response.getHeader(HttpHeader.CACHE_CONTROL.asString())); - - response = getResponse(); - - response.setStatus(200); - assertEquals(200, response.getStatus()); - assertEquals(null, response.getReason()); - - response = getResponse(); - - response.sendError(406, "Super Nanny"); - assertEquals(406, response.getStatus()); - assertEquals("Super Nanny", response.getReason()); - assertEquals("must-revalidate,no-cache,no-store", response.getHeader(HttpHeader.CACHE_CONTROL.asString())); + List data = new ArrayList<>(); + data.add(new Object[]{404, null, "Not Found"}); + data.add(new Object[]{500, "Database Error", "Database Error"}); + data.add(new Object[]{406, "Super Nanny", "Super Nanny"}); + return data.stream(); } - @Test - public void testStatusCodesNoErrorHandler() throws Exception + @ParameterizedTest + @MethodSource(value = "sendErrorTestCodes") + public void testStatusCodes(int code, String message, String expectedMessage) throws Exception + { + Response response = getResponse(); + assertThat(response.getHttpChannel().getState().handling(), is(HttpChannelState.Action.DISPATCH)); + + if (message == null) + response.sendError(code); + else + response.sendError(code, message); + + assertTrue(response.getHttpOutput().isClosed()); + assertEquals(code, response.getStatus()); + assertEquals(null, response.getReason()); + assertEquals(expectedMessage, response.getHttpChannel().getRequest().getAttribute(RequestDispatcher.ERROR_MESSAGE)); + assertThat(response.getHttpChannel().getState().unhandle(), is(HttpChannelState.Action.SEND_ERROR)); + assertThat(response.getHttpChannel().getState().unhandle(), is(HttpChannelState.Action.COMPLETE)); + } + + @ParameterizedTest + @MethodSource(value = "sendErrorTestCodes") + public void testStatusCodesNoErrorHandler(int code, String message, String expectedMessage) throws Exception { _server.removeBean(_server.getBean(ErrorHandler.class)); - Response response = getResponse(); - - response.sendError(404); - assertEquals(404, response.getStatus()); - assertEquals("Not Found", response.getReason()); - - response = getResponse(); - - response.sendError(500, "Database Error"); - assertEquals(500, response.getStatus()); - assertEquals("Database Error", response.getReason()); - assertThat(response.getHeader(HttpHeader.CACHE_CONTROL.asString()), Matchers.nullValue()); - - response = getResponse(); - - response.setStatus(200); - assertEquals(200, response.getStatus()); - assertEquals(null, response.getReason()); - - response = getResponse(); - - response.sendError(406, "Super Nanny"); - assertEquals(406, response.getStatus()); - assertEquals("Super Nanny", response.getReason()); - assertThat(response.getHeader(HttpHeader.CACHE_CONTROL.asString()), Matchers.nullValue()); + testStatusCodes(code, message, expectedMessage); } @Test @@ -898,7 +878,7 @@ public class ResponseTest assertTrue(!response.isCommitted()); assertTrue(!writer.checkError()); writer.print(""); - assertTrue(!writer.checkError()); + // assertTrue(!writer.checkError()); // TODO check if this is correct? checkout does an open check and the print above closes assertTrue(response.isCommitted()); } @@ -1032,7 +1012,7 @@ public class ResponseTest } @Test - public void testCookiesWithReset() throws Exception + public void testResetContent() throws Exception { Response response = getResponse(); @@ -1048,9 +1028,27 @@ public class ResponseTest cookie2.setPath("/path"); response.addCookie(cookie2); - //keep the cookies - response.reset(true); + response.setContentType("some/type"); + response.setContentLength(3); + response.setHeader(HttpHeader.EXPIRES,"never"); + response.setHeader("SomeHeader", "SomeValue"); + + response.getOutputStream(); + + // reset the content + response.resetContent(); + + // check content is nulled + assertThat(response.getContentType(), nullValue()); + assertThat(response.getContentLength(), is(-1L)); + assertThat(response.getHeader(HttpHeader.EXPIRES.asString()), nullValue()); + response.getWriter(); + + // check arbitrary header still set + assertThat(response.getHeader("SomeHeader"), is("SomeValue")); + + // check cookies are still there Enumeration set = response.getHttpFields().getValues("Set-Cookie"); assertNotNull(set); diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/NcsaRequestLogTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/NcsaRequestLogTest.java index 6964b7c9ef9..e854f3f44f1 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/NcsaRequestLogTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/NcsaRequestLogTest.java @@ -37,6 +37,7 @@ import org.eclipse.jetty.server.AbstractNCSARequestLog; import org.eclipse.jetty.server.CustomRequestLog; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpChannel; +import org.eclipse.jetty.server.HttpChannelState; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.RequestLog; @@ -44,6 +45,7 @@ import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.BlockingArrayQueue; import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.StacklessLogging; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; @@ -390,7 +392,7 @@ public class NcsaRequestLogTest data.add(new Object[]{logType, new IOExceptionPartialHandler(), "/ioex", "\"GET /ioex HTTP/1.0\" 200"}); data.add(new Object[]{logType, new RuntimeExceptionHandler(), "/rtex", "\"GET /rtex HTTP/1.0\" 500"}); data.add(new Object[]{logType, new BadMessageHandler(), "/bad", "\"GET /bad HTTP/1.0\" 499"}); - data.add(new Object[]{logType, new AbortHandler(), "/bad", "\"GET /bad HTTP/1.0\" 488"}); + data.add(new Object[]{logType, new AbortHandler(), "/bad", "\"GET /bad HTTP/1.0\" 500"}); data.add(new Object[]{logType, new AbortPartialHandler(), "/bad", "\"GET /bad HTTP/1.0\" 200"}); } @@ -517,7 +519,9 @@ public class NcsaRequestLogTest startServer(); makeRequest(requestPath); - expectedLogEntry = "\"GET " + requestPath + " HTTP/1.0\" 200"; + // If we abort, we can't write a 200 error page + if (!(testHandler instanceof AbortHandler)) + expectedLogEntry = expectedLogEntry.replaceFirst(" [1-9][0-9][0-9]", " 200"); assertRequestLog(expectedLogEntry, _log); } @@ -577,6 +581,10 @@ public class NcsaRequestLogTest { try { + while (baseRequest.getHttpChannel().getState().getState() != HttpChannelState.State.WAITING) + { + Thread.sleep(10); + } baseRequest.setHandled(false); testHandler.handle(target, baseRequest, request, response); if (!baseRequest.isHandled()) @@ -584,18 +592,21 @@ public class NcsaRequestLogTest } catch (BadMessageException bad) { - response.sendError(bad.getCode()); + response.sendError(bad.getCode(), bad.getReason()); } catch (Exception e) { - response.sendError(500); + response.sendError(500, e.toString()); } } - catch (Throwable th) + catch (IOException | IllegalStateException th) { - throw new RuntimeException(th); + Log.getLog().ignore(th); + } + finally + { + ac.complete(); } - ac.complete(); }); } } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/SecuredRedirectHandlerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/SecuredRedirectHandlerTest.java index 4dd9dfbfa1a..73d5193f262 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/SecuredRedirectHandlerTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/SecuredRedirectHandlerTest.java @@ -266,6 +266,7 @@ public class SecuredRedirectHandlerTest { if (!"/".equals(target)) { + baseRequest.setHandled(true); response.sendError(404); return; } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java index 5c68b9ef802..27c5d89251e 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java @@ -20,7 +20,6 @@ package org.eclipse.jetty.server.ssl; import java.io.File; import java.io.FileNotFoundException; -import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; @@ -42,6 +41,7 @@ import javax.net.ssl.SSLSocketFactory; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; @@ -55,8 +55,8 @@ import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SocketCustomizationListener; import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.server.handler.ErrorHandler; import org.eclipse.jetty.util.IO; -import org.eclipse.jetty.util.Utf8StringBuilder; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; @@ -65,9 +65,8 @@ import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.startsWith; +import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; public class SniSslConnectionFactoryTest { @@ -81,23 +80,23 @@ public class SniSslConnectionFactoryTest { _server = new Server(); - HttpConfiguration http_config = new HttpConfiguration(); - http_config.setSecureScheme("https"); - http_config.setSecurePort(8443); - http_config.setOutputBufferSize(32768); - _httpsConfiguration = new HttpConfiguration(http_config); + HttpConfiguration httpConfig = new HttpConfiguration(); + httpConfig.setSecureScheme("https"); + httpConfig.setSecurePort(8443); + httpConfig.setOutputBufferSize(32768); + _httpsConfiguration = new HttpConfiguration(httpConfig); SecureRequestCustomizer src = new SecureRequestCustomizer(); src.setSniHostCheck(true); _httpsConfiguration.addCustomizer(src); - _httpsConfiguration.addCustomizer((connector, httpConfig, request) -> + _httpsConfiguration.addCustomizer((connector, hc, request) -> { EndPoint endp = request.getHttpChannel().getEndPoint(); if (endp instanceof SslConnection.DecryptedEndPoint) { try { - SslConnection.DecryptedEndPoint ssl_endp = (SslConnection.DecryptedEndPoint)endp; - SslConnection sslConnection = ssl_endp.getSslConnection(); + SslConnection.DecryptedEndPoint sslEndp = (SslConnection.DecryptedEndPoint)endp; + SslConnection sslConnection = sslEndp.getSslConnection(); SSLEngine sslEngine = sslConnection.getSSLEngine(); SSLSession session = sslEngine.getSession(); for (Certificate c : session.getLocalCertificates()) @@ -224,6 +223,7 @@ public class SniSslConnectionFactoryTest public void testSameConnectionRequestsForManyDomains() throws Exception { start("src/test/resources/keystore_sni.p12"); + _server.setErrorHandler(new ErrorHandler()); SslContextFactory clientContextFactory = new SslContextFactory.Client(true); clientContextFactory.start(); @@ -246,8 +246,8 @@ public class SniSslConnectionFactoryTest output.flush(); InputStream input = sslSocket.getInputStream(); - String response = response(input); - assertTrue(response.startsWith("HTTP/1.1 200 ")); + HttpTester.Response response = HttpTester.parseResponse(input); + assertThat(response.getStatus(), is(200)); // Same socket, send a request for a different domain but same alias. request = @@ -256,9 +256,8 @@ public class SniSslConnectionFactoryTest "\r\n"; output.write(request.getBytes(StandardCharsets.UTF_8)); output.flush(); - - response = response(input); - assertTrue(response.startsWith("HTTP/1.1 200 ")); + response = HttpTester.parseResponse(input); + assertThat(response.getStatus(), is(200)); // Same socket, send a request for a different domain but different alias. request = @@ -268,9 +267,9 @@ public class SniSslConnectionFactoryTest output.write(request.getBytes(StandardCharsets.UTF_8)); output.flush(); - response = response(input); - assertThat(response, startsWith("HTTP/1.1 400 ")); - assertThat(response, containsString("Host does not match SNI")); + response = HttpTester.parseResponse(input); + assertThat(response.getStatus(), is(400)); + assertThat(response.getContent(), containsString("Host does not match SNI")); } finally { @@ -303,8 +302,8 @@ public class SniSslConnectionFactoryTest output.flush(); InputStream input = sslSocket.getInputStream(); - String response = response(input); - assertTrue(response.startsWith("HTTP/1.1 200 ")); + HttpTester.Response response = HttpTester.parseResponse(input); + assertThat(response.getStatus(), is(200)); // Now, on the same socket, send a request for a different valid domain. request = @@ -314,8 +313,8 @@ public class SniSslConnectionFactoryTest output.write(request.getBytes(StandardCharsets.UTF_8)); output.flush(); - response = response(input); - assertTrue(response.startsWith("HTTP/1.1 200 ")); + response = HttpTester.parseResponse(input); + assertThat(response.getStatus(), is(200)); // Now make a request for an invalid domain for this connection. request = @@ -325,9 +324,9 @@ public class SniSslConnectionFactoryTest output.write(request.getBytes(StandardCharsets.UTF_8)); output.flush(); - response = response(input); - assertTrue(response.startsWith("HTTP/1.1 400 ")); - assertThat(response, Matchers.containsString("Host does not match SNI")); + response = HttpTester.parseResponse(input); + assertThat(response.getStatus(), is(400)); + assertThat(response.getContent(), containsString("Host does not match SNI")); } finally { @@ -335,22 +334,6 @@ public class SniSslConnectionFactoryTest } } - private String response(InputStream input) throws IOException - { - Utf8StringBuilder buffer = new Utf8StringBuilder(); - int crlfs = 0; - while (true) - { - int read = input.read(); - assertTrue(read >= 0); - buffer.append((byte)read); - crlfs = (read == '\r' || read == '\n') ? crlfs + 1 : 0; - if (crlfs == 4) - break; - } - return buffer.toString(); - } - private String getResponse(String host, String cn) throws Exception { String response = getResponse(host, host, cn); diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncContextTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncContextTest.java index 1ae360927b5..6cc49bd60da 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncContextTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncContextTest.java @@ -50,6 +50,7 @@ import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -478,7 +479,7 @@ public class AsyncContextTest assertThat("error servlet", responseBody, containsString("ERROR: /error")); assertThat("error servlet", responseBody, containsString("PathInfo= /500")); - assertThat("error servlet", responseBody, containsString("EXCEPTION: java.lang.RuntimeException: TEST")); + assertThat("error servlet", responseBody, not(containsString("EXCEPTION: "))); } private class DispatchingRunnable implements Runnable @@ -552,7 +553,7 @@ public class AsyncContextTest @Override public void onTimeout(AsyncEvent event) throws IOException { - throw new RuntimeException("TEST"); + throw new RuntimeException("BAD EXPIRE"); } @Override diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncListenerTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncListenerTest.java index 50fc3a3f2b1..6d6bb5824ed 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncListenerTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncListenerTest.java @@ -32,6 +32,7 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpTester; +import org.eclipse.jetty.io.QuietException; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.QuietServletException; import org.eclipse.jetty.server.Server; @@ -42,6 +43,7 @@ import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertEquals; public class AsyncListenerTest @@ -140,7 +142,7 @@ public class AsyncListenerTest test_StartAsync_Throw_OnError(event -> { HttpServletResponse response = (HttpServletResponse)event.getAsyncContext().getResponse(); - response.sendError(HttpStatus.BAD_GATEWAY_502); + response.sendError(HttpStatus.BAD_GATEWAY_502, "Message!!!"); }); String httpResponse = connector.getResponse( "GET /ctx/path HTTP/1.1\r\n" + @@ -148,7 +150,8 @@ public class AsyncListenerTest "Connection: close\r\n" + "\r\n"); assertThat(httpResponse, containsString("HTTP/1.1 502 ")); - assertThat(httpResponse, containsString(TestRuntimeException.class.getName())); + assertThat(httpResponse, containsString("Message!!!")); + assertThat(httpResponse, not(containsString(TestRuntimeException.class.getName()))); } @Test @@ -191,7 +194,7 @@ public class AsyncListenerTest protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { AsyncContext asyncContext = request.startAsync(); - asyncContext.setTimeout(0); + asyncContext.setTimeout(10000); asyncContext.addListener(new AsyncListenerAdapter() { @Override @@ -268,7 +271,8 @@ public class AsyncListenerTest "Connection: close\r\n" + "\r\n"); assertThat(httpResponse, containsString("HTTP/1.1 500 ")); - assertThat(httpResponse, containsString(TestRuntimeException.class.getName())); + assertThat(httpResponse, containsString("AsyncContext timeout")); + assertThat(httpResponse, not(containsString(TestRuntimeException.class.getName()))); } @Test @@ -292,6 +296,7 @@ public class AsyncListenerTest { HttpServletResponse response = (HttpServletResponse)event.getAsyncContext().getResponse(); response.sendError(HttpStatus.BAD_GATEWAY_502); + event.getAsyncContext().complete(); }); String httpResponse = connector.getResponse( "GET / HTTP/1.1\r\n" + @@ -384,7 +389,7 @@ public class AsyncListenerTest protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { AsyncContext asyncContext = request.startAsync(); - asyncContext.setTimeout(0); + asyncContext.setTimeout(10000); asyncContext.addListener(new AsyncListenerAdapter() { @Override @@ -447,7 +452,7 @@ public class AsyncListenerTest } // Unique named RuntimeException to help during debugging / assertions. - public static class TestRuntimeException extends RuntimeException + public static class TestRuntimeException extends RuntimeException implements QuietException { } diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletIOTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletIOTest.java index 3ea4c804437..b80394f61f6 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletIOTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletIOTest.java @@ -52,6 +52,7 @@ import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.thread.QueuedThreadPool; @@ -305,6 +306,7 @@ public class AsyncServletIOTest request.append(s).append("w=").append(w); s = '&'; } + LOG.debug("process {} {}", request.toString(), BufferUtil.toDetailString(BufferUtil.toBuffer(content))); request.append(" HTTP/1.1\r\n") .append("Host: localhost\r\n") @@ -816,13 +818,15 @@ public class AsyncServletIOTest // wait until server is ready _servletStolenAsyncRead.ready.await(); final CountDownLatch wait = new CountDownLatch(1); - + final CountDownLatch held = new CountDownLatch(1); // Stop any dispatches until we want them + UnaryOperator old = _wQTP.wrapper.getAndSet(r -> () -> { try { + held.countDown(); wait.await(); r.run(); } @@ -836,7 +840,9 @@ public class AsyncServletIOTest // We are an unrelated thread, let's mess with the input stream ServletInputStream sin = _servletStolenAsyncRead.listener.in; sin.setReadListener(_servletStolenAsyncRead.listener); + // thread should be dispatched to handle, but held by our wQTP wait. + assertTrue(held.await(10, TimeUnit.SECONDS)); // Let's steal our read assertTrue(sin.isReady()); diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletTest.java index d197aa3a57b..571d8d76a7a 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletTest.java @@ -265,7 +265,6 @@ public class AsyncServletTest "start", "onTimeout", "error", - "onError", "ERROR /ctx/error/custom", "!initial", "onComplete")); @@ -273,44 +272,6 @@ public class AsyncServletTest assertContains("ERROR DISPATCH", response); } - @Test - public void testStartOnTimeoutErrorComplete() throws Exception - { - String response = process("start=200&timeout=error&error=complete", null); - assertThat(response, startsWith("HTTP/1.1 200 OK")); - assertThat(__history, contains( - "REQUEST /ctx/path/info", - "initial", - "start", - "onTimeout", - "error", - "onError", - "complete", - "onComplete")); - - assertContains("COMPLETED", response); - } - - @Test - public void testStartOnTimeoutErrorDispatch() throws Exception - { - String response = process("start=200&timeout=error&error=dispatch", null); - assertThat(response, startsWith("HTTP/1.1 200 OK")); - assertThat(__history, contains( - "REQUEST /ctx/path/info", - "initial", - "start", - "onTimeout", - "error", - "onError", - "dispatch", - "ASYNC /ctx/path/info", - "!initial", - "onComplete")); - - assertContains("DISPATCHED", response); - } - @Test public void testStartOnTimeoutComplete() throws Exception { @@ -526,8 +487,10 @@ public class AsyncServletTest "onStartAsync", "start", "onTimeout", + "ERROR /ctx/path/error", + "!initial", "onComplete")); // Error Page Loop! - assertContains("HTTP ERROR 500", response); + assertContains("AsyncContext timeout", response); } @Test diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/CustomRequestLogTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/CustomRequestLogTest.java index a422cf0480f..3c7ba9a7a55 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/CustomRequestLogTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/CustomRequestLogTest.java @@ -85,7 +85,8 @@ public class CustomRequestLogTest _connector.getResponse("GET /context/servlet/info HTTP/1.0\n\n"); String log = _entries.poll(5, TimeUnit.SECONDS); - assertThat(log, is("Filename: " + _tmpDir + File.separator + "servlet" + File.separator + "info")); + String expected = new File(_tmpDir + File.separator + "servlet" + File.separator + "info").getCanonicalPath(); + assertThat(log, is("Filename: " + expected)); } @Test diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java index 899cc3d0169..0d8db7f6e55 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java @@ -20,8 +20,22 @@ package org.eclipse.jetty.servlet; import java.io.IOException; import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.EnumSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import javax.servlet.AsyncContext; +import javax.servlet.DispatcherType; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; import javax.servlet.Servlet; import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -29,8 +43,12 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.server.Dispatcher; import org.eclipse.jetty.server.HttpChannel; +import org.eclipse.jetty.server.HttpChannelState; import org.eclipse.jetty.server.LocalConnector; +import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.HandlerWrapper; +import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.StacklessLogging; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; @@ -40,43 +58,71 @@ import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertTrue; public class ErrorPageTest { private Server _server; private LocalConnector _connector; private StacklessLogging _stackless; + private static CountDownLatch __asyncSendErrorCompleted; + private ErrorPageErrorHandler _errorPageErrorHandler; @BeforeEach public void init() throws Exception { _server = new Server(); _connector = new LocalConnector(_server); + _server.addConnector(_connector); + ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS); - _server.addConnector(_connector); _server.setHandler(context); context.setContextPath("/"); + context.addFilter(SingleDispatchFilter.class, "/*", EnumSet.allOf(DispatcherType.class)); + context.addServlet(DefaultServlet.class, "/"); context.addServlet(FailServlet.class, "/fail/*"); context.addServlet(FailClosedServlet.class, "/fail-closed/*"); context.addServlet(ErrorServlet.class, "/error/*"); context.addServlet(AppServlet.class, "/app/*"); context.addServlet(LongerAppServlet.class, "/longer.app/*"); + context.addServlet(SyncSendErrorServlet.class, "/sync/*"); + context.addServlet(AsyncSendErrorServlet.class, "/async/*"); + context.addServlet(NotEnoughServlet.class, "/notenough/*"); + context.addServlet(DeleteServlet.class, "/delete/*"); + context.addServlet(ErrorAndStatusServlet.class, "/error-and-status/*"); - ErrorPageErrorHandler error = new ErrorPageErrorHandler(); - context.setErrorHandler(error); - error.addErrorPage(599, "/error/599"); - error.addErrorPage(400, "/error/400"); + HandlerWrapper noopHandler = new HandlerWrapper() + { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + if (target.startsWith("/noop")) + return; + else + super.handle(target, baseRequest, request, response); + } + }; + context.insertHandler(noopHandler); + + _errorPageErrorHandler = new ErrorPageErrorHandler(); + context.setErrorHandler(_errorPageErrorHandler); + _errorPageErrorHandler.addErrorPage(595, "/error/595"); + _errorPageErrorHandler.addErrorPage(597, "/sync"); + _errorPageErrorHandler.addErrorPage(599, "/error/599"); + _errorPageErrorHandler.addErrorPage(400, "/error/400"); // error.addErrorPage(500,"/error/500"); - error.addErrorPage(IllegalStateException.class.getCanonicalName(), "/error/TestException"); - error.addErrorPage(BadMessageException.class, "/error/BadMessageException"); - error.addErrorPage(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE, "/error/GlobalErrorPage"); + _errorPageErrorHandler.addErrorPage(IllegalStateException.class.getCanonicalName(), "/error/TestException"); + _errorPageErrorHandler.addErrorPage(BadMessageException.class, "/error/BadMessageException"); + _errorPageErrorHandler.addErrorPage(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE, "/error/GlobalErrorPage"); _server.start(); _stackless = new StacklessLogging(ServletHandler.class); + + __asyncSendErrorCompleted = new CountDownLatch(1); } @AfterEach @@ -87,6 +133,101 @@ public class ErrorPageTest _server.join(); } + @Test + void testErrorOverridesStatus() throws Exception + { + String response = _connector.getResponse("GET /error-and-status/anything HTTP/1.0\r\n\r\n"); + assertThat(response, Matchers.containsString("HTTP/1.1 594 594")); + assertThat(response, Matchers.containsString("ERROR_PAGE: /GlobalErrorPage")); + assertThat(response, Matchers.containsString("ERROR_MESSAGE: custom get error")); + assertThat(response, Matchers.containsString("ERROR_CODE: 594")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION: null")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION_TYPE: null")); + assertThat(response, Matchers.containsString("ERROR_SERVLET: org.eclipse.jetty.servlet.ErrorPageTest$ErrorAndStatusServlet-")); + assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /error-and-status/anything")); + } + + @Test + void testHttp204CannotHaveBody() throws Exception + { + String response = _connector.getResponse("GET /fail/code?code=204 HTTP/1.0\r\n\r\n"); + assertThat(response, Matchers.containsString("HTTP/1.1 204 No Content")); + assertThat(response, not(Matchers.containsString("DISPATCH: "))); + assertThat(response, not(Matchers.containsString("ERROR_PAGE: "))); + assertThat(response, not(Matchers.containsString("ERROR_CODE: "))); + assertThat(response, not(Matchers.containsString("ERROR_EXCEPTION: "))); + assertThat(response, not(Matchers.containsString("ERROR_EXCEPTION_TYPE: "))); + assertThat(response, not(Matchers.containsString("ERROR_SERVLET: "))); + assertThat(response, not(Matchers.containsString("ERROR_REQUEST_URI: "))); + } + + @Test + void testDeleteCannotHaveBody() throws Exception + { + String response = _connector.getResponse("DELETE /delete/anything HTTP/1.0\r\n\r\n"); + assertThat(response, Matchers.containsString("HTTP/1.1 595 595")); + assertThat(response, not(Matchers.containsString("DISPATCH: "))); + assertThat(response, not(Matchers.containsString("ERROR_PAGE: "))); + assertThat(response, not(Matchers.containsString("ERROR_MESSAGE: "))); + assertThat(response, not(Matchers.containsString("ERROR_CODE: "))); + assertThat(response, not(Matchers.containsString("ERROR_EXCEPTION: "))); + assertThat(response, not(Matchers.containsString("ERROR_EXCEPTION_TYPE: "))); + assertThat(response, not(Matchers.containsString("ERROR_SERVLET: "))); + assertThat(response, not(Matchers.containsString("ERROR_REQUEST_URI: "))); + + assertThat(response, not(containsString("This shouldn't be seen"))); + } + + @Test + void testGenerateAcceptableResponse_noAcceptHeader() throws Exception + { + // no global error page here + _errorPageErrorHandler.getErrorPages().remove(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE); + + String response = _connector.getResponse("GET /fail/code?code=598 HTTP/1.0\r\n\r\n"); + assertThat(response, Matchers.containsString("HTTP/1.1 598 598")); + assertThat(response, Matchers.containsString("Error 598")); + assertThat(response, Matchers.containsString("<h2>HTTP ERROR 598")); + assertThat(response, Matchers.containsString("/fail/code")); + } + + @Test + void testGenerateAcceptableResponse_htmlAcceptHeader() throws Exception + { + // no global error page here + _errorPageErrorHandler.getErrorPages().remove(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE); + + // even when text/html is not the 1st content type, a html error page should still be generated + String response = _connector.getResponse("GET /fail/code?code=598 HTTP/1.0\r\n" + + "Accept: application/bytes,text/html\r\n\r\n"); + assertThat(response, Matchers.containsString("HTTP/1.1 598 598")); + assertThat(response, Matchers.containsString("<title>Error 598")); + assertThat(response, Matchers.containsString("<h2>HTTP ERROR 598")); + assertThat(response, Matchers.containsString("/fail/code")); + } + + @Test + void testGenerateAcceptableResponse_noHtmlAcceptHeader() throws Exception + { + // no global error page here + _errorPageErrorHandler.getErrorPages().remove(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE); + + String response = _connector.getResponse("GET /fail/code?code=598 HTTP/1.0\r\n" + + "Accept: application/bytes\r\n\r\n"); + assertThat(response, Matchers.containsString("HTTP/1.1 598 598")); + assertThat(response, not(Matchers.containsString("<title>Error 598"))); + assertThat(response, not(Matchers.containsString("<h2>HTTP ERROR 598"))); + assertThat(response, not(Matchers.containsString("/fail/code"))); + } + + @Test + void testNestedSendErrorDoesNotLoop() throws Exception + { + String response = _connector.getResponse("GET /fail/code?code=597 HTTP/1.0\r\n\r\n"); + assertThat(response, Matchers.containsString("HTTP/1.1 597 597")); + assertThat(response, not(Matchers.containsString("time this error page is being accessed"))); + } + @Test public void testSendErrorClosedResponse() throws Exception { @@ -167,7 +308,7 @@ public class ErrorPageTest try (StacklessLogging ignore = new StacklessLogging(Dispatcher.class)) { String response = _connector.getResponse("GET /app?baa=%88%A4 HTTP/1.0\r\n\r\n"); - assertThat(response, Matchers.containsString("HTTP/1.1 400 Bad query encoding")); + assertThat(response, Matchers.containsString("HTTP/1.1 400 Bad Request")); assertThat(response, Matchers.containsString("ERROR_PAGE: /BadMessageException")); assertThat(response, Matchers.containsString("ERROR_MESSAGE: Bad query encoding")); assertThat(response, Matchers.containsString("ERROR_CODE: 400")); @@ -179,6 +320,94 @@ public class ErrorPageTest } } + @Test + public void testAsyncErrorPageDSC() throws Exception + { + try (StacklessLogging ignore = new StacklessLogging(Dispatcher.class)) + { + String response = _connector.getResponse("GET /async/info?mode=DSC HTTP/1.0\r\n\r\n"); + assertThat(response, Matchers.containsString("HTTP/1.1 599 599")); + assertThat(response, Matchers.containsString("ERROR_PAGE: /599")); + assertThat(response, Matchers.containsString("ERROR_CODE: 599")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION: null")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION_TYPE: null")); + assertThat(response, Matchers.containsString("ERROR_SERVLET: org.eclipse.jetty.servlet.ErrorPageTest$AsyncSendErrorServlet-")); + assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /async/info")); + assertTrue(__asyncSendErrorCompleted.await(10, TimeUnit.SECONDS)); + } + } + + @Test + public void testAsyncErrorPageSDC() throws Exception + { + try (StacklessLogging ignore = new StacklessLogging(Dispatcher.class)) + { + String response = _connector.getResponse("GET /async/info?mode=SDC HTTP/1.0\r\n\r\n"); + assertThat(response, Matchers.containsString("HTTP/1.1 599 599")); + assertThat(response, Matchers.containsString("ERROR_PAGE: /599")); + assertThat(response, Matchers.containsString("ERROR_CODE: 599")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION: null")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION_TYPE: null")); + assertThat(response, Matchers.containsString("ERROR_SERVLET: org.eclipse.jetty.servlet.ErrorPageTest$AsyncSendErrorServlet-")); + assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /async/info")); + assertTrue(__asyncSendErrorCompleted.await(10, TimeUnit.SECONDS)); + } + } + + @Test + public void testAsyncErrorPageSCD() throws Exception + { + try (StacklessLogging ignore = new StacklessLogging(Dispatcher.class)) + { + String response = _connector.getResponse("GET /async/info?mode=SCD HTTP/1.0\r\n\r\n"); + assertThat(response, Matchers.containsString("HTTP/1.1 599 599")); + assertThat(response, Matchers.containsString("ERROR_PAGE: /599")); + assertThat(response, Matchers.containsString("ERROR_CODE: 599")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION: null")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION_TYPE: null")); + assertThat(response, Matchers.containsString("ERROR_SERVLET: org.eclipse.jetty.servlet.ErrorPageTest$AsyncSendErrorServlet-")); + assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /async/info")); + assertTrue(__asyncSendErrorCompleted.await(10, TimeUnit.SECONDS)); + } + } + + @Test + public void testNoop() throws Exception + { + String response = _connector.getResponse("GET /noop/info HTTP/1.0\r\n\r\n"); + assertThat(response, Matchers.containsString("HTTP/1.1 404 Not Found")); + assertThat(response, Matchers.containsString("DISPATCH: ERROR")); + assertThat(response, Matchers.containsString("ERROR_PAGE: /GlobalErrorPage")); + assertThat(response, Matchers.containsString("ERROR_CODE: 404")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION: null")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION_TYPE: null")); + assertThat(response, Matchers.containsString("ERROR_SERVLET: org.eclipse.jetty.servlet.DefaultServlet-")); + assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /noop/info")); + } + + @Test + public void testNotEnough() throws Exception + { + String response = _connector.getResponse("GET /notenough/info HTTP/1.0\r\n\r\n"); + assertThat(response, Matchers.containsString("HTTP/1.1 500 Server Error")); + assertThat(response, Matchers.containsString("DISPATCH: ERROR")); + assertThat(response, Matchers.containsString("ERROR_PAGE: /GlobalErrorPage")); + assertThat(response, Matchers.containsString("ERROR_CODE: 500")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION: null")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION_TYPE: null")); + assertThat(response, Matchers.containsString("ERROR_SERVLET: org.eclipse.jetty.servlet.ErrorPageTest$NotEnoughServlet-")); + assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /notenough/info")); + } + + @Test + public void testNotEnoughCommitted() throws Exception + { + String response = _connector.getResponse("GET /notenough/info?commit=true HTTP/1.0\r\n\r\n"); + assertThat(response, Matchers.containsString("HTTP/1.1 200 OK")); + assertThat(response, Matchers.containsString("Content-Length: 1000")); + assertThat(response, Matchers.endsWith("SomeBytes")); + } + public static class AppServlet extends HttpServlet implements Servlet { @Override @@ -198,6 +427,112 @@ public class ErrorPageTest } } + public static class SyncSendErrorServlet extends HttpServlet implements Servlet + { + public static final AtomicInteger COUNTER = new AtomicInteger(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + int count = COUNTER.incrementAndGet(); + + PrintWriter writer = response.getWriter(); + writer.println("this is the " + count + " time this error page is being accessed"); + response.sendError(597, "loop #" + count); + } + } + + public static class AsyncSendErrorServlet extends HttpServlet implements Servlet + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + try + { + final CountDownLatch hold = new CountDownLatch(1); + final String mode = request.getParameter("mode"); + switch(mode) + { + case "DSC": + case "SDC": + case "SCD": + break; + default: + throw new IllegalStateException(mode); + } + + final boolean lateComplete = "true".equals(request.getParameter("latecomplete")); + AsyncContext async = request.startAsync(); + async.start(() -> + { + try + { + switch(mode) + { + case "SDC": + response.sendError(599); + break; + case "SCD": + response.sendError(599); + async.complete(); + break; + default: + break; + } + + // Complete after original servlet + hold.countDown(); + + // Wait until request async waiting + while (Request.getBaseRequest(request).getHttpChannelState().getState() == HttpChannelState.State.HANDLING) + { + try + { + Thread.sleep(10); + } + catch (InterruptedException e) + { + e.printStackTrace(); + } + } + try + { + switch (mode) + { + case "DSC": + response.sendError(599); + async.complete(); + break; + case "SDC": + async.complete(); + break; + default: + break; + } + } + catch(IllegalStateException e) + { + Log.getLog().ignore(e); + } + finally + { + __asyncSendErrorCompleted.countDown(); + } + } + catch (IOException e) + { + Log.getLog().warn(e); + } + }); + hold.await(); + } + catch (InterruptedException e) + { + throw new ServletException(e); + } + } + } + public static class FailServlet extends HttpServlet implements Servlet { @Override @@ -225,16 +560,51 @@ public class ErrorPageTest } catch (Throwable ignore) { - // no opEchoSocket + Log.getLog().ignore(ignore); } } } + public static class ErrorAndStatusServlet extends HttpServlet implements Servlet + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + response.sendError(594, "custom get error"); + response.setStatus(200); + } + } + + public static class DeleteServlet extends HttpServlet implements Servlet + { + @Override + protected void doDelete(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + response.getWriter().append("This shouldn't be seen"); + response.sendError(595, "custom delete"); + } + } + + public static class NotEnoughServlet extends HttpServlet implements Servlet + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + response.setContentLength(1000); + response.getOutputStream().write("SomeBytes".getBytes(StandardCharsets.UTF_8)); + if (Boolean.parseBoolean(request.getParameter("commit"))) + response.flushBuffer(); + } + } + public static class ErrorServlet extends HttpServlet implements Servlet { @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + if (request.getDispatcherType() != DispatcherType.ERROR && request.getDispatcherType() != DispatcherType.ASYNC) + throw new IllegalStateException("Bad Dispatcher Type " + request.getDispatcherType()); + PrintWriter writer = response.getWriter(); writer.println("DISPATCH: " + request.getDispatcherType().name()); writer.println("ERROR_PAGE: " + request.getPathInfo()); @@ -247,4 +617,55 @@ public class ErrorPageTest writer.println("getParameterMap()= " + request.getParameterMap()); } } + + public static class SingleDispatchFilter implements Filter + { + ConcurrentMap<Integer, Thread> dispatches = new ConcurrentHashMap<>(); + + @Override + public void init(FilterConfig filterConfig) throws ServletException + { + + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException + { + final Integer key = request.hashCode(); + Thread current = Thread.currentThread(); + final Thread existing = dispatches.putIfAbsent(key, current); + if (existing != null && existing != current) + { + System.err.println("DOUBLE DISPATCH OF REQUEST!!!!!!!!!!!!!!!!!!"); + System.err.println("Thread " + existing + " :"); + for (StackTraceElement element : existing.getStackTrace()) + { + System.err.println("\tat " + element); + } + IllegalStateException ex = new IllegalStateException(); + ex.printStackTrace(); + response.flushBuffer(); + throw ex; + } + + try + { + chain.doFilter(request, response); + } + finally + { + if (existing == null) + { + if (!dispatches.remove(key, current)) + throw new IllegalStateException(); + } + } + } + + @Override + public void destroy() + { + + } + } } diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerTest.java index cf8e7d59261..1362ac6f4d4 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerTest.java @@ -153,7 +153,10 @@ public class GzipHandlerTest response.setHeader("ETag", __contentETag); String ifnm = req.getHeader("If-None-Match"); if (ifnm != null && ifnm.equals(__contentETag)) - response.sendError(304); + { + response.setStatus(304); + response.flushBuffer(); + } else { PrintWriter writer = response.getWriter(); diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/DoSFilter.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/DoSFilter.java index fcdff1bdec8..d15f57dc3cc 100644 --- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/DoSFilter.java +++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/DoSFilter.java @@ -501,7 +501,16 @@ public class DoSFilter implements Filter { if (LOG.isDebugEnabled()) LOG.debug("Timing out {}", request); - response.sendError(HttpStatus.SERVICE_UNAVAILABLE_503); + try + { + response.sendError(HttpStatus.SERVICE_UNAVAILABLE_503); + } + catch (IllegalStateException ise) + { + LOG.ignore(ise); + // abort instead + response.sendError(-1); + } } catch (Throwable x) { diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java b/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java index a0eceef6307..452c1be848c 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java @@ -440,6 +440,7 @@ public class BufferUtil * * @param to Buffer is flush mode * @param b byte to append + * @throws BufferOverflowException if unable to append buffer due to space limits */ public static void append(ByteBuffer to, byte b) { @@ -1103,20 +1104,20 @@ public class BufferUtil for (int i = 0; i < buffer.position(); i++) { appendContentChar(buf, buffer.get(i)); - if (i == 16 && buffer.position() > 32) + if (i == 8 && buffer.position() > 16) { buf.append("..."); - i = buffer.position() - 16; + i = buffer.position() - 8; } } buf.append("<<<"); for (int i = buffer.position(); i < buffer.limit(); i++) { appendContentChar(buf, buffer.get(i)); - if (i == buffer.position() + 16 && buffer.limit() > buffer.position() + 32) + if (i == buffer.position() + 24 && buffer.limit() > buffer.position() + 48) { buf.append("..."); - i = buffer.limit() - 16; + i = buffer.limit() - 24; } } buf.append(">>>"); @@ -1125,10 +1126,10 @@ public class BufferUtil for (int i = limit; i < buffer.capacity(); i++) { appendContentChar(buf, buffer.get(i)); - if (i == limit + 16 && buffer.capacity() > limit + 32) + if (i == limit + 8 && buffer.capacity() > limit + 16) { buf.append("..."); - i = buffer.capacity() - 16; + i = buffer.capacity() - 8; } } buffer.limit(limit); diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java index c2d77760c08..41f02b80e8b 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java @@ -171,10 +171,11 @@ public class QueuedThreadPool extends ContainerLifeCycle implements SizedThreadP if (LOG.isDebugEnabled()) LOG.debug("Stopping {}", this); + super.doStop(); + removeBean(_tryExecutor); _tryExecutor = TryExecutor.NO_TRY; - super.doStop(); // Signal the Runner threads that we are stopping int threads = _counts.getAndSetHi(Integer.MIN_VALUE); diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketConnectionStatsTest.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketConnectionStatsTest.java index 41e79c01b86..148720950c4 100644 --- a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketConnectionStatsTest.java +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketConnectionStatsTest.java @@ -176,4 +176,4 @@ public class WebSocketConnectionStatsTest assertThat("stats.sendBytes", statistics.getSentBytes(), is(expectedSent)); assertThat("stats.receivedBytes", statistics.getReceivedBytes(), is(expectedReceived)); } -} \ No newline at end of file +} diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketNegotiationTest.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketNegotiationTest.java index 9c23e097ccb..27ec3c2c45a 100644 --- a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketNegotiationTest.java +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketNegotiationTest.java @@ -115,7 +115,7 @@ public class WebSocketNegotiationTest client.getOutputStream().write(upgradeRequest.getBytes(ISO_8859_1)); String response = getUpgradeResponse(client.getInputStream()); - assertThat(response, containsString("400 Missing request header 'Sec-WebSocket-Key'")); + assertThat(response, containsString("400 ")); } protected static HttpFields newUpgradeRequest(String extensions) diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketInvalidVersionTest.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketInvalidVersionTest.java index 105de580dc2..36989de4607 100644 --- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketInvalidVersionTest.java +++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketInvalidVersionTest.java @@ -89,6 +89,6 @@ public class WebSocketInvalidVersionTest connFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT); }); assertThat(x.getCause(), instanceOf(UpgradeException.class)); - assertThat(x.getMessage(), containsString("400 Unsupported websocket version specification")); + assertThat(x.getMessage(), containsString("400 ")); } } diff --git a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/BadAppTests.java b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/BadAppTests.java index 190bc381743..9248a2dc5e4 100644 --- a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/BadAppTests.java +++ b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/BadAppTests.java @@ -108,8 +108,8 @@ public class BadAppTests extends AbstractDistributionTest startHttpClient(); ContentResponse response = client.GET("http://localhost:" + port + "/badapp/"); assertEquals(HttpStatus.SERVICE_UNAVAILABLE_503, response.getStatus()); - assertThat(response.getContentAsString(), containsString("Unavailable")); - assertThat(response.getContentAsString(), containsString("Problem accessing /badapp/")); + assertThat(response.getContentAsString(), containsString("<h2>HTTP ERROR 503 Service Unavailable</h2>")); + assertThat(response.getContentAsString(), containsString("<tr><th>URI:</th><td>/badapp/</td></tr>")); } } } @@ -148,8 +148,8 @@ public class BadAppTests extends AbstractDistributionTest startHttpClient(); ContentResponse response = client.GET("http://localhost:" + port + "/badapp/"); assertEquals(HttpStatus.SERVICE_UNAVAILABLE_503, response.getStatus()); - assertThat(response.getContentAsString(), containsString("Unavailable")); - assertThat(response.getContentAsString(), containsString("Problem accessing /badapp/")); + assertThat(response.getContentAsString(), containsString("<h2>HTTP ERROR 503 Service Unavailable</h2>")); + assertThat(response.getContentAsString(), containsString("<tr><th>URI:</th><td>/badapp/</td></tr>")); } } } From 149d9d486227a9f18cba19e8d307ff0e5197ed9c Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt <joakim.erdfelt@gmail.com> Date: Mon, 26 Aug 2019 13:40:32 -0500 Subject: [PATCH 25/55] Fixes #4020 - Revert ExtensionFactory change to interface. + The change in commit 30dc103a129d08304e1f1a99a84c045049de305d was done to allow the InflaterPool and DeflaterPool to be managed by the Jetty lifecycle. + This restore the original abstract class ExtensionFactory. + Had to break the traditional LifeCycle usage for a more non-traditional one in order to both, not break this existing API, and not introduce jetty-util to the webapp classloader. + This will restore API / binary compatibility for other projects, like spring-boot. Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com> --- .../api/extensions/ExtensionFactory.java | 58 ++++++++++++--- .../extensions/WebSocketExtensionFactory.java | 71 ++++++++++++++++++- 2 files changed, 118 insertions(+), 11 deletions(-) diff --git a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/ExtensionFactory.java b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/ExtensionFactory.java index eae3dd3fbe6..8d46d60a5eb 100644 --- a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/ExtensionFactory.java +++ b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/ExtensionFactory.java @@ -18,22 +18,64 @@ package org.eclipse.jetty.websocket.api.extensions; +import java.util.HashMap; +import java.util.Iterator; import java.util.Map; +import java.util.ServiceLoader; import java.util.Set; -public interface ExtensionFactory extends Iterable<Class<? extends Extension>> +public abstract class ExtensionFactory implements Iterable<Class<? extends Extension>> { - Map<String, Class<? extends Extension>> getAvailableExtensions(); + private ServiceLoader<Extension> extensionLoader = ServiceLoader.load(Extension.class); + private Map<String, Class<? extends Extension>> availableExtensions; - Class<? extends Extension> getExtension(String name); + public ExtensionFactory() + { + availableExtensions = new HashMap<>(); + for (Extension ext : extensionLoader) + { + if (ext != null) + { + availableExtensions.put(ext.getName(), ext.getClass()); + } + } + } - Set<String> getExtensionNames(); + public Map<String, Class<? extends Extension>> getAvailableExtensions() + { + return availableExtensions; + } - boolean isAvailable(String name); + public Class<? extends Extension> getExtension(String name) + { + return availableExtensions.get(name); + } - Extension newInstance(ExtensionConfig config); + public Set<String> getExtensionNames() + { + return availableExtensions.keySet(); + } - void register(String name, Class<? extends Extension> extension); + public boolean isAvailable(String name) + { + return availableExtensions.containsKey(name); + } - void unregister(String name); + @Override + public Iterator<Class<? extends Extension>> iterator() + { + return availableExtensions.values().iterator(); + } + + public abstract Extension newInstance(ExtensionConfig config); + + public void register(String name, Class<? extends Extension> extension) + { + availableExtensions.put(name, extension); + } + + public void unregister(String name) + { + availableExtensions.remove(name); + } } diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/WebSocketExtensionFactory.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/WebSocketExtensionFactory.java index f382d687afa..706429567b8 100644 --- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/WebSocketExtensionFactory.java +++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/WebSocketExtensionFactory.java @@ -27,6 +27,7 @@ import java.util.zip.Deflater; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.component.ContainerLifeCycle; +import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.compression.CompressionPool; import org.eclipse.jetty.util.compression.DeflaterPool; import org.eclipse.jetty.util.compression.InflaterPool; @@ -37,8 +38,9 @@ import org.eclipse.jetty.websocket.api.extensions.ExtensionFactory; import org.eclipse.jetty.websocket.common.extensions.compress.CompressExtension; import org.eclipse.jetty.websocket.common.scopes.WebSocketContainerScope; -public class WebSocketExtensionFactory extends ContainerLifeCycle implements ExtensionFactory +public class WebSocketExtensionFactory extends ExtensionFactory implements LifeCycle { + private ContainerLifeCycle lifecycle; private WebSocketContainerScope container; private ServiceLoader<Extension> extensionLoader = ServiceLoader.load(Extension.class); private Map<String, Class<? extends Extension>> availableExtensions; @@ -47,6 +49,7 @@ public class WebSocketExtensionFactory extends ContainerLifeCycle implements Ext public WebSocketExtensionFactory(WebSocketContainerScope container) { + lifecycle = new ContainerLifeCycle(); availableExtensions = new HashMap<>(); for (Extension ext : extensionLoader) { @@ -55,8 +58,8 @@ public class WebSocketExtensionFactory extends ContainerLifeCycle implements Ext } this.container = container; - addBean(inflaterPool); - addBean(deflaterPool); + lifecycle.addBean(inflaterPool); + lifecycle.addBean(deflaterPool); } @Override @@ -144,4 +147,66 @@ public class WebSocketExtensionFactory extends ContainerLifeCycle implements Ext { return availableExtensions.values().iterator(); } + + /* --- All of the below ugliness due to not being able to break API compatibility with ExtensionFactory --- */ + + @Override + public void start() throws Exception + { + lifecycle.start(); + } + + @Override + public void stop() throws Exception + { + lifecycle.stop(); + } + + @Override + public boolean isRunning() + { + return lifecycle.isRunning(); + } + + @Override + public boolean isStarted() + { + return lifecycle.isStarted(); + } + + @Override + public boolean isStarting() + { + return lifecycle.isStarting(); + } + + @Override + public boolean isStopping() + { + return lifecycle.isStopping(); + } + + @Override + public boolean isStopped() + { + return lifecycle.isStopped(); + } + + @Override + public boolean isFailed() + { + return lifecycle.isFailed(); + } + + @Override + public void addLifeCycleListener(Listener listener) + { + lifecycle.addLifeCycleListener(listener); + } + + @Override + public void removeLifeCycleListener(Listener listener) + { + lifecycle.removeLifeCycleListener(listener); + } } From 8f6e028f052a10d78d282dbafbfa4abab5325291 Mon Sep 17 00:00:00 2001 From: Greg Wilkins <gregw@webtide.com> Date: Tue, 27 Aug 2019 08:41:00 +1000 Subject: [PATCH 26/55] remove duplicate ignores Signed-off-by: Greg Wilkins <gregw@webtide.com> --- jetty-maven-plugin/.gitignore | 5 ----- jetty-unixsocket/.gitignore | 1 - jetty-util/.gitignore | 7 ------- tests/test-webapps/.gitignore | 1 - 4 files changed, 14 deletions(-) delete mode 100644 jetty-maven-plugin/.gitignore delete mode 100644 jetty-unixsocket/.gitignore delete mode 100644 jetty-util/.gitignore delete mode 100644 tests/test-webapps/.gitignore diff --git a/jetty-maven-plugin/.gitignore b/jetty-maven-plugin/.gitignore deleted file mode 100644 index 929d903c7d3..00000000000 --- a/jetty-maven-plugin/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.classpath -.project -.settings -target -*.swp diff --git a/jetty-unixsocket/.gitignore b/jetty-unixsocket/.gitignore deleted file mode 100644 index b83d22266ac..00000000000 --- a/jetty-unixsocket/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target/ diff --git a/jetty-util/.gitignore b/jetty-util/.gitignore deleted file mode 100644 index 89c493b64bf..00000000000 --- a/jetty-util/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -.project -.classpath -.settings/ -.pmd -target/ -*.swp -*.log diff --git a/tests/test-webapps/.gitignore b/tests/test-webapps/.gitignore deleted file mode 100644 index e73b9a8a351..00000000000 --- a/tests/test-webapps/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*/src/main/webapp/META-INF From 5164f7bb4175faa872ab91129dd27e043a532c2c Mon Sep 17 00:00:00 2001 From: Greg Wilkins <gregw@webtide.com> Date: Tue, 27 Aug 2019 10:52:27 +1000 Subject: [PATCH 27/55] fixed merge Signed-off-by: Greg Wilkins <gregw@webtide.com> --- .../org/eclipse/jetty/server/HttpChannel.java | 59 ++++++++++--------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index 11b0e8b3c37..21b27f69047 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -487,14 +487,14 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor // RFC 7230, section 3.3. if (!_request.isHead() && !_response.isContentComplete(_response.getHttpOutput().getWritten())) - sendErrorOrAbort("Insufficient content written"); - - + { + if (sendErrorOrAbort("Insufficient content written")) + break; + } // TODO Currently a blocking/aborting consumeAll is done in the handling of the TERMINATED // TODO Action triggered by the completed callback below. It would be possible to modify the // TODO callback to do a non-blocking consumeAll at this point and only call completed when // TODO that is done. - checkAndPrepareUpgrade(); // Set a close callback on the HttpOutput to make it an async callback @@ -525,20 +525,25 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor return !suspended; } - public void sendErrorOrAbort(String message) + public boolean sendErrorOrAbort(String message) { try { if (isCommitted()) + { abort(new IOException(message)); - else - _response.sendError(HttpStatus.INTERNAL_SERVER_ERROR_500, message); + return false; + } + + _response.sendError(HttpStatus.INTERNAL_SERVER_ERROR_500, message); + return true; } catch (Throwable x) { LOG.ignore(x); abort(x); } + return false; } private void dispatch(DispatcherType type, Dispatchable dispatchable) throws IOException, ServletException @@ -628,8 +633,6 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor _request.setHandled(true); _state.completing(); sendResponse(null, _response.getHttpOutput().getBuffer(), true, Callback.from(_state::completed)); - { - } } catch (Throwable x) { @@ -758,9 +761,9 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor public void onBadMessage(BadMessageException failure) { int status = failure.getCode(); - String message = failure.getReason(); + String reason = failure.getReason(); if (status < HttpStatus.BAD_REQUEST_400 || status > 599) - failure = new BadMessageException(HttpStatus.BAD_REQUEST_400, message, failure); + failure = new BadMessageException(HttpStatus.BAD_REQUEST_400, reason, failure); notifyRequestFailure(_request, failure); @@ -786,9 +789,9 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor ErrorHandler handler = getServer().getBean(ErrorHandler.class); if (handler != null) - content = handler.badMessageError(status, message, fields); + content = handler.badMessageError(status, reason, fields); - sendResponse(new MetaData.Response(HttpVersion.HTTP_1_1, status, null, fields, BufferUtil.length(content)), content, true); + sendResponse(new MetaData.Response(HttpVersion.HTTP_1_1, status, reason, fields, BufferUtil.length(content)), content, true); } } catch (IOException e) @@ -1127,7 +1130,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onRequestBegin(Request request) + default void onRequestBegin(Request request) { } @@ -1136,7 +1139,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onBeforeDispatch(Request request) + default void onBeforeDispatch(Request request) { } @@ -1146,7 +1149,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * @param request the request object * @param failure the exception thrown by the application */ - public default void onDispatchFailure(Request request, Throwable failure) + default void onDispatchFailure(Request request, Throwable failure) { } @@ -1155,7 +1158,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onAfterDispatch(Request request) + default void onAfterDispatch(Request request) { } @@ -1166,7 +1169,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * @param request the request object * @param content a {@link ByteBuffer#slice() slice} of the request content chunk */ - public default void onRequestContent(Request request, ByteBuffer content) + default void onRequestContent(Request request, ByteBuffer content) { } @@ -1175,7 +1178,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onRequestContentEnd(Request request) + default void onRequestContentEnd(Request request) { } @@ -1184,7 +1187,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onRequestTrailers(Request request) + default void onRequestTrailers(Request request) { } @@ -1193,7 +1196,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onRequestEnd(Request request) + default void onRequestEnd(Request request) { } @@ -1203,7 +1206,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * @param request the request object * @param failure the request failure */ - public default void onRequestFailure(Request request, Throwable failure) + default void onRequestFailure(Request request, Throwable failure) { } @@ -1212,7 +1215,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onResponseBegin(Request request) + default void onResponseBegin(Request request) { } @@ -1223,7 +1226,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onResponseCommit(Request request) + default void onResponseCommit(Request request) { } @@ -1233,7 +1236,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * @param request the request object * @param content a {@link ByteBuffer#slice() slice} of the response content chunk */ - public default void onResponseContent(Request request, ByteBuffer content) + default void onResponseContent(Request request, ByteBuffer content) { } @@ -1242,7 +1245,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onResponseEnd(Request request) + default void onResponseEnd(Request request) { } @@ -1252,7 +1255,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * @param request the request object * @param failure the response failure */ - public default void onResponseFailure(Request request, Throwable failure) + default void onResponseFailure(Request request, Throwable failure) { } @@ -1261,7 +1264,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onComplete(Request request) + default void onComplete(Request request) { } } From 387e33acaf0dec60bb9c311ac83f7ac02f6243e4 Mon Sep 17 00:00:00 2001 From: Jan Bartel <janb@webtide.com> Date: Tue, 27 Aug 2019 10:53:54 +1000 Subject: [PATCH 28/55] Issue #4006 Fix ClusteredSessionMigrationTest (#4010) Signed-off-by: Jan Bartel <janb@webtide.com> --- .../ClusteredSessionMigrationTest.java | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ClusteredSessionMigrationTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ClusteredSessionMigrationTest.java index c8a5937f597..31627854634 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ClusteredSessionMigrationTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ClusteredSessionMigrationTest.java @@ -81,8 +81,14 @@ public class ClusteredSessionMigrationTest extends AbstractTestBase server1.start(); int port1 = server1.getPort(); - TestServer server2 = new TestServer(0, TestServer.DEFAULT_MAX_INACTIVE, TestServer.DEFAULT_SCAVENGE_SEC, - cacheFactory, storeFactory); + //Configure a cache and store same way for server2 + DefaultSessionCacheFactory cacheFactory2 = new DefaultSessionCacheFactory(); + cacheFactory2.setEvictionPolicy(SessionCache.NEVER_EVICT); + cacheFactory2.setSaveOnCreate(true); + SessionDataStoreFactory storeFactory2 = createSessionDataStoreFactory(); + + TestServer server2 = new TestServer(0,TestServer.DEFAULT_MAX_INACTIVE, TestServer.DEFAULT_SCAVENGE_SEC, + cacheFactory2, storeFactory2); server2.addContext(contextPath).addServlet(TestServlet.class, servletMapping); try @@ -110,8 +116,6 @@ public class ClusteredSessionMigrationTest extends AbstractTestBase request2.header("Cookie", sessionCookie); ContentResponse response2 = request2.send(); assertEquals(HttpServletResponse.SC_OK, response2.getStatus()); - String response = response2.getContentAsString(); - assertEquals(response.trim(), String.valueOf(value)); } finally { @@ -132,6 +136,8 @@ public class ClusteredSessionMigrationTest extends AbstractTestBase public static class TestServlet extends HttpServlet { private static final long serialVersionUID = 1L; + + private static long createTime = 0; @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException @@ -149,6 +155,7 @@ public class ClusteredSessionMigrationTest extends AbstractTestBase { if (session == null) session = request.getSession(true); + createTime = session.getCreationTime(); int value = Integer.parseInt(request.getParameter("value")); session.setAttribute("value", value); PrintWriter writer = response.getWriter(); @@ -157,12 +164,12 @@ public class ClusteredSessionMigrationTest extends AbstractTestBase } else if ("get".equals(action)) { - int value = (Integer)session.getAttribute("value"); - int x = session.getMaxInactiveInterval(); - assertTrue(x > 0); - PrintWriter writer = response.getWriter(); - writer.println(value); - writer.flush(); + //We cannot test if the session contains the attribute node1 + //set, because it may not have finished writing the attribute + //by the time its response returned to the client and the request + //was sent to node2. Just test the create time, which should be + //saved. + assertEquals(createTime, session.getCreationTime()); } } } From 19980ceeb5eb6f18eb88c17b3f6e61565a4d9bae Mon Sep 17 00:00:00 2001 From: Jan Bartel <janb@webtide.com> Date: Tue, 27 Aug 2019 11:00:09 +1000 Subject: [PATCH 29/55] Issue #4009 ServletContextHandler setSecurityHandler broke handler chain (#4012) * Issue #4009 ServletContextHandler setSecurityHandler broke handler chain Signed-off-by: Jan Bartel <janb@webtide.com> --- .../jetty/servlet/ServletContextHandler.java | 2 +- .../servlet/ServletContextHandlerTest.java | 70 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletContextHandler.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletContextHandler.java index 365f6fc70f4..ba98aee7515 100644 --- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletContextHandler.java +++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletContextHandler.java @@ -609,7 +609,7 @@ public class ServletContextHandler extends ContextHandler */ public void setSecurityHandler(SecurityHandler securityHandler) { - replaceHandler(_sessionHandler, securityHandler); + replaceHandler(_securityHandler, securityHandler); _securityHandler = securityHandler; relinkHandlers(); } diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ServletContextHandlerTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ServletContextHandlerTest.java index beb3c2dcdf0..c870a679b94 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ServletContextHandlerTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ServletContextHandlerTest.java @@ -52,10 +52,13 @@ import javax.servlet.http.HttpSessionIdListener; import javax.servlet.http.HttpSessionListener; import org.eclipse.jetty.security.ConstraintSecurityHandler; +import org.eclipse.jetty.security.RoleInfo; import org.eclipse.jetty.security.SecurityHandler; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.UserIdentity; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.handler.AbstractHandlerContainer; import org.eclipse.jetty.server.handler.ContextHandler; @@ -81,6 +84,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -703,6 +707,72 @@ public class ServletContextHandlerTest assertThat("Response", response, containsString("Hello World")); } + @Test + public void testSetSecurityHandler() throws Exception + { + ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS|ServletContextHandler.SECURITY|ServletContextHandler.GZIP); + assertNotNull(context.getSessionHandler()); + SessionHandler sessionHandler = context.getSessionHandler(); + assertNotNull(context.getSecurityHandler()); + SecurityHandler securityHandler = context.getSecurityHandler(); + assertNotNull(context.getGzipHandler()); + GzipHandler gzipHandler = context.getGzipHandler(); + + //check the handler linking order + HandlerWrapper h = (HandlerWrapper)context.getHandler(); + assertSame(h, sessionHandler); + + h = (HandlerWrapper)h.getHandler(); + assertSame(h, securityHandler); + + h = (HandlerWrapper)h.getHandler(); + assertSame(h, gzipHandler); + + //replace the security handler + SecurityHandler myHandler = new SecurityHandler() + { + @Override + protected RoleInfo prepareConstraintInfo(String pathInContext, Request request) + { + return null; + } + + @Override + protected boolean checkUserDataPermissions(String pathInContext, Request request, Response response, + RoleInfo constraintInfo) throws IOException + { + return false; + } + + @Override + protected boolean isAuthMandatory(Request baseRequest, Response baseResponse, Object constraintInfo) + { + return false; + } + + @Override + protected boolean checkWebResourcePermissions(String pathInContext, Request request, Response response, + Object constraintInfo, UserIdentity userIdentity) + throws IOException + { + return false; + } + }; + + //check the linking order + context.setSecurityHandler(myHandler); + assertSame(myHandler, context.getSecurityHandler()); + + h = (HandlerWrapper)context.getHandler(); + assertSame(h, sessionHandler); + + h = (HandlerWrapper)h.getHandler(); + assertSame(h, myHandler); + + h = (HandlerWrapper)h.getHandler(); + assertSame(h, gzipHandler); + } + @Test public void testReplaceServletHandlerWithoutServlet() throws Exception { From 15e2f7226446f1a9f4ba4a6fb4bbd94f8be303e2 Mon Sep 17 00:00:00 2001 From: Greg Wilkins <gregw@webtide.com> Date: Tue, 27 Aug 2019 11:33:06 +1000 Subject: [PATCH 30/55] updated after merge to fix tests Signed-off-by: Greg Wilkins <gregw@webtide.com> --- .../jetty/http2/server/HTTP2ServerConnection.java | 5 ++--- .../jetty/http2/server/HttpTransportOverHTTP2.java | 5 +++-- .../java/org/eclipse/jetty/server/HttpChannel.java | 13 ++++++++++--- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java index 2dd10e670e6..4173eb5f1d0 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java @@ -376,10 +376,9 @@ public class HTTP2ServerConnection extends HTTP2Connection implements Connection } @Override - protected void checkAndPrepareUpgrade() + protected boolean checkAndPrepareUpgrade() { - if (isTunnel()) - getHttpTransport().prepareUpgrade(); + return isTunnel() && getHttpTransport().prepareUpgrade(); } @Override diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java index 1f53feba058..2664adceb7a 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java @@ -322,7 +322,7 @@ public class HttpTransportOverHTTP2 implements HttpTransport return transportCallback.onIdleTimeout(failure); } - void prepareUpgrade() + boolean prepareUpgrade() { HttpChannelOverHTTP2 channel = (HttpChannelOverHTTP2)stream.getAttachment(); Request request = channel.getRequest(); @@ -331,7 +331,8 @@ public class HttpTransportOverHTTP2 implements HttpTransport endPoint.upgrade(connection); stream.setAttachment(endPoint); if (request.getHttpInput().hasContent()) - channel.sendErrorOrAbort("Unexpected content in CONNECT request"); + return channel.sendErrorOrAbort("Unexpected content in CONNECT request"); + return true; } @Override diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index 21b27f69047..0df6668f113 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -491,11 +491,15 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor if (sendErrorOrAbort("Insufficient content written")) break; } + + // Check if an update is done (if so, do not close) + if (checkAndPrepareUpgrade()) + break; + // TODO Currently a blocking/aborting consumeAll is done in the handling of the TERMINATED // TODO Action triggered by the completed callback below. It would be possible to modify the // TODO callback to do a non-blocking consumeAll at this point and only call completed when // TODO that is done. - checkAndPrepareUpgrade(); // Set a close callback on the HttpOutput to make it an async callback _response.closeOutput(Callback.from(_state::completed)); @@ -731,9 +735,12 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * response is sent back to the client.</p> * <p>This avoids a race where the server is unprepared if the client sends * data immediately after having received the upgrade response.</p> + * @return true if the channel is not complete and more processing is required, + * either because the upgrade has succeeded or sendError has been called. */ - protected void checkAndPrepareUpgrade() + protected boolean checkAndPrepareUpgrade() { + return false; } public void onCompleted() @@ -791,7 +798,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor if (handler != null) content = handler.badMessageError(status, reason, fields); - sendResponse(new MetaData.Response(HttpVersion.HTTP_1_1, status, reason, fields, BufferUtil.length(content)), content, true); + sendResponse(new MetaData.Response(HttpVersion.HTTP_1_1, status, null, fields, BufferUtil.length(content)), content, true); } } catch (IOException e) From d216792d2304065856a21fc2d744afcc91ee719b Mon Sep 17 00:00:00 2001 From: Greg Wilkins <gregw@webtide.com> Date: Tue, 27 Aug 2019 13:01:34 +1000 Subject: [PATCH 31/55] Made test not fail in symlinked directory Signed-off-by: Greg Wilkins <gregw@webtide.com> --- .../test/java/org/eclipse/jetty/util/TypeUtilTest.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/TypeUtilTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/TypeUtilTest.java index b723d195385..e296152ef6d 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/TypeUtilTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/TypeUtilTest.java @@ -21,6 +21,8 @@ package org.eclipse.jetty.util; import java.nio.file.Path; import java.nio.file.Paths; +import org.eclipse.jetty.util.resource.Resource; +import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnJre; import org.junit.jupiter.api.condition.EnabledOnJre; @@ -160,16 +162,15 @@ public class TypeUtilTest } @Test - public void testGetLocationOfClass_FromMavenRepo() + public void testGetLocationOfClass_FromMavenRepo() throws Exception { String mavenRepoPathProperty = System.getProperty("mavenRepoPath"); assumeTrue(mavenRepoPathProperty != null); Path mavenRepoPath = Paths.get(mavenRepoPathProperty); - String mavenRepo = mavenRepoPath.toFile().getPath().replaceAll("\\\\", "/"); - // Classes from maven dependencies - assertThat(TypeUtil.getLocationOfClass(org.junit.jupiter.api.Assertions.class).toASCIIString(), containsString(mavenRepo)); + Resource resource = Resource.newResource(TypeUtil.getLocationOfClass(org.junit.jupiter.api.Assertions.class).toASCIIString()); + assertThat(resource.getFile().getCanonicalPath(), Matchers.startsWith(mavenRepoPath.toFile().getCanonicalPath())); } @Test From b51d770807e89e1a35c7ce92280e5d18218efcbe Mon Sep 17 00:00:00 2001 From: Olivier Lamy <oliver.lamy@gmail.com> Date: Tue, 27 Aug 2019 13:03:28 +1000 Subject: [PATCH 32/55] session#getLastAccessedTime should throw IllegalStateException if session has been invalidated (#4023) * per servlet api javadoc getLastAccessedTime should throw IllegalStateException if session has been invalidated Signed-off-by: olivier lamy <oliver.lamy@gmail.com> * isInvalid test should be done within lock Signed-off-by: olivier lamy <oliver.lamy@gmail.com> --- .../eclipse/jetty/server/session/Session.java | 4 ++++ .../jetty/server/session/SessionHandlerTest.java | 16 ++++++++++++++-- 2 files changed, 18 insertions(+), 2 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 440883d23db..867afb9bad8 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 @@ -483,6 +483,10 @@ public class Session implements SessionHandler.SessionIf { try (Lock lock = _lock.lock()) { + if (isInvalid()) + { + throw new IllegalStateException("Session not valid"); + } return _sessionData.getLastAccessed(); } } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/session/SessionHandlerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/session/SessionHandlerTest.java index 351ede0d227..e119dad6f33 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/session/SessionHandlerTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/session/SessionHandlerTest.java @@ -23,9 +23,10 @@ import java.util.Collections; import java.util.HashSet; import javax.servlet.SessionTrackingMode; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertThrows; + public class SessionHandlerTest { @Test @@ -34,7 +35,18 @@ public class SessionHandlerTest SessionHandler sessionHandler = new SessionHandler(); sessionHandler.setSessionTrackingModes(new HashSet<>(Arrays.asList(SessionTrackingMode.COOKIE, SessionTrackingMode.URL))); sessionHandler.setSessionTrackingModes(Collections.singleton(SessionTrackingMode.SSL)); - Assertions.assertThrows(IllegalArgumentException.class,() -> + assertThrows(IllegalArgumentException.class,() -> sessionHandler.setSessionTrackingModes(new HashSet<>(Arrays.asList(SessionTrackingMode.SSL, SessionTrackingMode.URL)))); } + + @Test + public void testInvalidSessiongetLastAccessedTime() + { + Session session = new Session(new SessionHandler(), + new SessionData("sd" ,"", "", 0, 0, 0, 0)); + session.getLastAccessedTime(); + session.invalidate(); + assertThrows(IllegalStateException.class, () -> session.getLastAccessedTime()); + } + } From 5403a30b329bf923ee2e928ce5742340630fe9a1 Mon Sep 17 00:00:00 2001 From: Olivier Lamy <oliver.lamy@gmail.com> Date: Tue, 27 Aug 2019 13:03:28 +1000 Subject: [PATCH 33/55] session#getLastAccessedTime should throw IllegalStateException if session has been invalidated (#4023) * per servlet api javadoc getLastAccessedTime should throw IllegalStateException if session has been invalidated Signed-off-by: olivier lamy <oliver.lamy@gmail.com> * isInvalid test should be done within lock Signed-off-by: olivier lamy <oliver.lamy@gmail.com> --- .../eclipse/jetty/server/session/Session.java | 4 ++++ .../jetty/server/session/SessionHandlerTest.java | 16 ++++++++++++++-- 2 files changed, 18 insertions(+), 2 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 06a85108e39..1052f3b2364 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 @@ -476,6 +476,10 @@ public class Session implements SessionHandler.SessionIf { try (Lock lock = _lock.lock()) { + if (isInvalid()) + { + throw new IllegalStateException("Session not valid"); + } return _sessionData.getLastAccessed(); } } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/session/SessionHandlerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/session/SessionHandlerTest.java index 351ede0d227..e119dad6f33 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/session/SessionHandlerTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/session/SessionHandlerTest.java @@ -23,9 +23,10 @@ import java.util.Collections; import java.util.HashSet; import javax.servlet.SessionTrackingMode; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertThrows; + public class SessionHandlerTest { @Test @@ -34,7 +35,18 @@ public class SessionHandlerTest SessionHandler sessionHandler = new SessionHandler(); sessionHandler.setSessionTrackingModes(new HashSet<>(Arrays.asList(SessionTrackingMode.COOKIE, SessionTrackingMode.URL))); sessionHandler.setSessionTrackingModes(Collections.singleton(SessionTrackingMode.SSL)); - Assertions.assertThrows(IllegalArgumentException.class,() -> + assertThrows(IllegalArgumentException.class,() -> sessionHandler.setSessionTrackingModes(new HashSet<>(Arrays.asList(SessionTrackingMode.SSL, SessionTrackingMode.URL)))); } + + @Test + public void testInvalidSessiongetLastAccessedTime() + { + Session session = new Session(new SessionHandler(), + new SessionData("sd" ,"", "", 0, 0, 0, 0)); + session.getLastAccessedTime(); + session.invalidate(); + assertThrows(IllegalStateException.class, () -> session.getLastAccessedTime()); + } + } From 1f189d4618877983f528ac7beee80fcbf9dc8b50 Mon Sep 17 00:00:00 2001 From: Greg Wilkins <gregw@webtide.com> Date: Tue, 27 Aug 2019 14:27:46 +1000 Subject: [PATCH 34/55] fixed test visibility for JPMS Signed-off-by: Greg Wilkins <gregw@webtide.com> --- .../org/eclipse/jetty/servlet/ErrorPageTest.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java index 0d8db7f6e55..a82e7a63452 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java @@ -134,7 +134,7 @@ public class ErrorPageTest } @Test - void testErrorOverridesStatus() throws Exception + public void testErrorOverridesStatus() throws Exception { String response = _connector.getResponse("GET /error-and-status/anything HTTP/1.0\r\n\r\n"); assertThat(response, Matchers.containsString("HTTP/1.1 594 594")); @@ -148,7 +148,7 @@ public class ErrorPageTest } @Test - void testHttp204CannotHaveBody() throws Exception + public void testHttp204CannotHaveBody() throws Exception { String response = _connector.getResponse("GET /fail/code?code=204 HTTP/1.0\r\n\r\n"); assertThat(response, Matchers.containsString("HTTP/1.1 204 No Content")); @@ -162,7 +162,7 @@ public class ErrorPageTest } @Test - void testDeleteCannotHaveBody() throws Exception + public void testDeleteCannotHaveBody() throws Exception { String response = _connector.getResponse("DELETE /delete/anything HTTP/1.0\r\n\r\n"); assertThat(response, Matchers.containsString("HTTP/1.1 595 595")); @@ -179,7 +179,7 @@ public class ErrorPageTest } @Test - void testGenerateAcceptableResponse_noAcceptHeader() throws Exception + public void testGenerateAcceptableResponse_noAcceptHeader() throws Exception { // no global error page here _errorPageErrorHandler.getErrorPages().remove(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE); @@ -192,7 +192,7 @@ public class ErrorPageTest } @Test - void testGenerateAcceptableResponse_htmlAcceptHeader() throws Exception + public void testGenerateAcceptableResponse_htmlAcceptHeader() throws Exception { // no global error page here _errorPageErrorHandler.getErrorPages().remove(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE); @@ -207,7 +207,7 @@ public class ErrorPageTest } @Test - void testGenerateAcceptableResponse_noHtmlAcceptHeader() throws Exception + public void testGenerateAcceptableResponse_noHtmlAcceptHeader() throws Exception { // no global error page here _errorPageErrorHandler.getErrorPages().remove(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE); @@ -221,7 +221,7 @@ public class ErrorPageTest } @Test - void testNestedSendErrorDoesNotLoop() throws Exception + public void testNestedSendErrorDoesNotLoop() throws Exception { String response = _connector.getResponse("GET /fail/code?code=597 HTTP/1.0\r\n\r\n"); assertThat(response, Matchers.containsString("HTTP/1.1 597 597")); From d890748f3afa76d03e4c5b41e750b3f732394e7a Mon Sep 17 00:00:00 2001 From: Greg Wilkins <gregw@webtide.com> Date: Tue, 27 Aug 2019 16:19:07 +1000 Subject: [PATCH 35/55] More merge fixes Signed-off-by: Greg Wilkins <gregw@webtide.com> --- .../org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java | 2 +- .../src/main/java/org/eclipse/jetty/server/HttpChannel.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java index 2664adceb7a..0dac59d2f06 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java @@ -332,7 +332,7 @@ public class HttpTransportOverHTTP2 implements HttpTransport stream.setAttachment(endPoint); if (request.getHttpInput().hasContent()) return channel.sendErrorOrAbort("Unexpected content in CONNECT request"); - return true; + return false; } @Override diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index 0df6668f113..c036d746728 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -736,7 +736,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * <p>This avoids a race where the server is unprepared if the client sends * data immediately after having received the upgrade response.</p> * @return true if the channel is not complete and more processing is required, - * either because the upgrade has succeeded or sendError has been called. + * typically because sendError has been called. */ protected boolean checkAndPrepareUpgrade() { From 83463c2a23563ec4ef2e95d52c0337dc2e3f0124 Mon Sep 17 00:00:00 2001 From: Greg Wilkins <gregw@webtide.com> Date: Tue, 27 Aug 2019 17:38:20 +1000 Subject: [PATCH 36/55] Issue #3964 - Listener behavior cleanup (Jetty 9.4.x) (#3965) Issue #3964 * Avoid creating listener list for rarely used requestAttributeListener * AbstractConnector keeps a specific list of HttpChannel.Listeners to avoid Connection.Listeners and MBean listeners being added to the HttpChannel listener list. * Simplified listener handling by avoiding null connector, previously only needed for testing. * Fixed test that assumed HttpChannel listeners were not cleared by a recycle * Separated out durable vs cyclic HttpChannel.Listeners, so as to simplify handling. * Deprecated cyclic HttpChannel.Listeners, as I'm not sure the channel is the right place for them. * Added improved method to combine multiple HttpChannel Listeners into a single Listener. * Fixed MockConnector * Added benchmark * Improved benchmark * Updates from review * Removed benchmark and alternate implementations. * Updated javadoc * Updates from review Signed-off-by: Greg Wilkins <gregw@webtide.com> --- .../SpnegoAuthenticatorTest.java | 30 +- .../jetty/server/AbstractConnector.java | 37 +++ .../org/eclipse/jetty/server/HttpChannel.java | 257 +++++++++------- .../jetty/server/HttpChannelListeners.java | 286 ++++++++++++++++++ .../org/eclipse/jetty/server/Request.java | 1 + .../jetty/server/HttpChannelEventTest.java | 31 ++ .../jetty/server/HttpInputAsyncStateTest.java | 2 +- .../eclipse/jetty/server/HttpInputTest.java | 2 +- .../eclipse/jetty/server/HttpWriterTest.java | 2 +- .../eclipse/jetty/server/MockConnector.java | 46 +++ .../jetty/util/component/Container.java | 2 +- ...tractClusteredInvalidationSessionTest.java | 4 +- ...bstractClusteredSessionScavengingTest.java | 8 +- .../AbstractWebAppObjectInSessionTest.java | 5 +- .../session/TestContextScopeListener.java | 76 ----- .../TestHttpChannelCompleteListener.java | 45 +++ .../jetty/server/session/TestServer.java | 6 + .../jetty/server/session/AsyncTest.java | 21 +- .../jetty/server/session/CreationTest.java | 24 +- .../session/DefaultSessionCacheTest.java | 5 +- .../session/DeleteUnloadableSessionTest.java | 4 +- .../server/session/DirtyAttributeTest.java | 8 +- .../jetty/server/session/IdleSessionTest.java | 8 +- .../NonClusteredSessionScavengingTest.java | 12 +- .../session/ReentrantRequestSessionTest.java | 4 +- .../SameContextForwardedSessionTest.java | 4 +- .../server/session/SaveOptimizeTest.java | 20 +- .../server/session/SessionRenewTest.java | 10 +- 28 files changed, 702 insertions(+), 258 deletions(-) create mode 100644 jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelListeners.java create mode 100644 jetty-server/src/test/java/org/eclipse/jetty/server/MockConnector.java delete mode 100644 tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/TestContextScopeListener.java create mode 100644 tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/TestHttpChannelCompleteListener.java diff --git a/jetty-security/src/test/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticatorTest.java b/jetty-security/src/test/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticatorTest.java index 00af87e7ecc..76a6bfa1c14 100644 --- a/jetty-security/src/test/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticatorTest.java +++ b/jetty-security/src/test/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticatorTest.java @@ -25,6 +25,7 @@ import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.Authentication; import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.HttpChannelState; @@ -58,7 +59,7 @@ public class SpnegoAuthenticatorTest @Test public void testChallengeSentWithNoAuthorization() throws Exception { - HttpChannel channel = new HttpChannel(null, new HttpConfiguration(), null, null) + HttpChannel channel = new HttpChannel(new MockConnector(), new HttpConfiguration(), null, null) { @Override public Server getServer() @@ -94,7 +95,7 @@ public class SpnegoAuthenticatorTest @Test public void testChallengeSentWithUnhandledAuthorization() throws Exception { - HttpChannel channel = new HttpChannel(null, new HttpConfiguration(), null, null) + HttpChannel channel = new HttpChannel(new MockConnector(), new HttpConfiguration(), null, null) { @Override public Server getServer() @@ -162,4 +163,29 @@ public class SpnegoAuthenticatorTest assertEquals("negotiate", _authenticator.getAuthSchemeFromHeader(" negotiate asdfasdf")); assertEquals("negotiated", _authenticator.getAuthSchemeFromHeader(" negotiated asdfasdf")); } + + class MockConnector extends AbstractConnector + { + public MockConnector() + { + super(new Server() , null, null, null, 0); + } + + @Override + protected void accept(int acceptorID) throws IOException, InterruptedException + { + } + + @Override + public Object getTransport() + { + return null; + } + + @Override + public String dumpSelf() + { + return null; + } + } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java index 46c18701b7e..f1425dde301 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java @@ -45,6 +45,7 @@ import org.eclipse.jetty.util.ProcessorUtils; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedObject; +import org.eclipse.jetty.util.component.Container; import org.eclipse.jetty.util.component.ContainerLifeCycle; import org.eclipse.jetty.util.component.Dumpable; import org.eclipse.jetty.util.component.Graceful; @@ -154,6 +155,7 @@ public abstract class AbstractConnector extends ContainerLifeCycle implements Co private final Set<EndPoint> _endpoints = Collections.newSetFromMap(new ConcurrentHashMap<>()); private final Set<EndPoint> _immutableEndPoints = Collections.unmodifiableSet(_endpoints); private final Graceful.Shutdown _shutdown = new Graceful.Shutdown(); + private HttpChannel.Listener _httpChannelListeners = HttpChannel.NOOP_LISTENER; private CountDownLatch _stopping; private long _idleTimeout = 30000; private String _defaultProtocol; @@ -188,6 +190,23 @@ public abstract class AbstractConnector extends ContainerLifeCycle implements Co pool = _server.getBean(ByteBufferPool.class); _byteBufferPool = pool != null ? pool : new ArrayByteBufferPool(); + addEventListener(new Container.Listener() + { + @Override + public void beanAdded(Container parent, Object bean) + { + if (bean instanceof HttpChannel.Listener) + _httpChannelListeners = new HttpChannelListeners(getBeans(HttpChannel.Listener.class)); + } + + @Override + public void beanRemoved(Container parent, Object bean) + { + if (bean instanceof HttpChannel.Listener) + _httpChannelListeners = new HttpChannelListeners(getBeans(HttpChannel.Listener.class)); + } + }); + addBean(_server, false); addBean(_executor); if (executor == null) @@ -208,6 +227,24 @@ public abstract class AbstractConnector extends ContainerLifeCycle implements Co _acceptors = new Thread[acceptors]; } + /** + * Get the {@link HttpChannel.Listener}s added to the connector + * as a single combined Listener. + * This is equivalent to a listener that iterates over the individual + * listeners returned from <code>getBeans(HttpChannel.Listener.class);</code>, + * except that: <ul> + * <li>The result is precomputed, so it is more efficient</li> + * <li>The result is ordered by the order added.</li> + * <li>The result is immutable.</li> + * </ul> + * @see #getBeans(Class) + * @return An unmodifiable list of EventListener beans + */ + public HttpChannel.Listener getHttpChannelListeners() + { + return _httpChannelListeners; + } + @Override public Server getServer() { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index 31714973255..38760aadac4 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.EventListener; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.TimeoutException; @@ -69,6 +70,7 @@ import org.eclipse.jetty.util.thread.Scheduler; */ public class HttpChannel implements Runnable, HttpOutput.Interceptor { + public static Listener NOOP_LISTENER = new Listener(){}; private static final Logger LOG = Log.getLogger(HttpChannel.class); private final AtomicLong _requests = new AtomicLong(); @@ -80,9 +82,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor private final HttpChannelState _state; private final Request _request; private final Response _response; + private final HttpChannel.Listener _combinedListener; + @Deprecated + private final List<Listener> _transientListeners = new ArrayList<>(); private HttpFields _trailers; private final Supplier<HttpFields> _trailerSupplier = () -> _trailers; - private final List<Listener> _listeners; private MetaData.Response _committedMetaData; private RequestLog _requestLog; private long _oldIdleTimeout; @@ -103,13 +107,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor _request = new Request(this, newHttpInput(_state)); _response = new Response(this, newHttpOutput()); - _executor = connector == null ? null : connector.getServer().getThreadPool(); - _requestLog = connector == null ? null : connector.getServer().getRequestLog(); - - List<Listener> listeners = new ArrayList<>(); - if (connector != null) - listeners.addAll(connector.getBeans(Listener.class)); - _listeners = listeners; + _executor = connector.getServer().getThreadPool(); + _requestLog = connector.getServer().getRequestLog(); + _combinedListener = (connector instanceof AbstractConnector) + ? ((AbstractConnector)connector).getHttpChannelListeners() + : NOOP_LISTENER; if (LOG.isDebugEnabled()) LOG.debug("new {} -> {},{},{}", @@ -139,14 +141,32 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor return _state; } + /** + * Add a transient Listener to the HttpChannel. + * <p>Listeners added by this method will only be notified + * if the HttpChannel has been constructed with an instance of + * {@link TransientListeners} as an {@link AbstractConnector} + * provided listener</p> + * <p>Transient listeners are removed after every request cycle</p> + * @param listener + * @return true if the listener was added. + */ + @Deprecated public boolean addListener(Listener listener) { - return _listeners.add(listener); + return _transientListeners.add(listener); } + @Deprecated public boolean removeListener(Listener listener) { - return _listeners.remove(listener); + return _transientListeners.remove(listener); + } + + @Deprecated + public List<Listener> getTransientListeners() + { + return _transientListeners; } public long getBytesWritten() @@ -294,6 +314,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor _written = 0; _trailers = null; _oldIdleTimeout = 0; + _transientListeners.clear(); } public void onAsyncWaitForContent() @@ -535,17 +556,17 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor try { _request.setDispatcherType(type); - notifyBeforeDispatch(_request); + _combinedListener.onBeforeDispatch(_request); dispatchable.dispatch(); } catch (Throwable x) { - notifyDispatchFailure(_request, x); + _combinedListener.onDispatchFailure(_request, x); throw x; } finally { - notifyAfterDispatch(_request); + _combinedListener.onAfterDispatch(_request); _request.setDispatcherType(null); } } @@ -668,7 +689,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor _request.setSecure(HttpScheme.HTTPS.is(request.getURI().getScheme())); - notifyRequestBegin(_request); + _combinedListener.onRequestBegin(_request); if (LOG.isDebugEnabled()) LOG.debug("REQUEST for {} on {}{}{} {} {}{}{}", request.getURIString(), this, System.lineSeparator(), @@ -680,7 +701,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor { if (LOG.isDebugEnabled()) LOG.debug("onContent {} {}", this, content); - notifyRequestContent(_request, content.getByteBuffer()); + _combinedListener.onRequestContent(_request, content.getByteBuffer()); return _request.getHttpInput().addContent(content); } @@ -688,7 +709,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor { if (LOG.isDebugEnabled()) LOG.debug("onContentComplete {}", this); - notifyRequestContentEnd(_request); + _combinedListener.onRequestContentEnd(_request); return false; } @@ -697,7 +718,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor if (LOG.isDebugEnabled()) LOG.debug("onTrailers {} {}", this, trailers); _trailers = trailers; - notifyRequestTrailers(_request); + _combinedListener.onRequestTrailers(_request); } public boolean onRequestComplete() @@ -705,7 +726,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor if (LOG.isDebugEnabled()) LOG.debug("onRequestComplete {}", this); boolean result = _request.getHttpInput().eof(); - notifyRequestEnd(_request); + _combinedListener.onRequestEnd(_request); return result; } @@ -722,7 +743,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor setIdleTimeout(_oldIdleTimeout); _request.onCompleted(); - notifyComplete(_request); + _combinedListener.onComplete(_request); _transport.onCompleted(); } @@ -738,7 +759,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor if (status < HttpStatus.BAD_REQUEST_400 || status > 599) failure = new BadMessageException(HttpStatus.BAD_REQUEST_400, reason, failure); - notifyRequestFailure(_request, failure); + _combinedListener.onRequestFailure(_request, failure); Action action; try @@ -810,7 +831,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor ? new Send100Callback(callback) : new SendCallback(callback, content, true, complete); - notifyResponseBegin(_request); + _combinedListener.onResponseBegin(_request); // committing write _transport.send(info, _request.isHead(), content, complete, committed); @@ -936,89 +957,14 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor { if (_state.abortResponse()) { - notifyResponseFailure(_request, failure); + _combinedListener.onResponseFailure(_request, failure); _transport.abort(failure); } } - private void notifyRequestBegin(Request request) - { - notifyEvent1(listener -> listener::onRequestBegin, request); - } - - private void notifyBeforeDispatch(Request request) - { - notifyEvent1(listener -> listener::onBeforeDispatch, request); - } - - private void notifyDispatchFailure(Request request, Throwable failure) - { - notifyEvent2(listener -> listener::onDispatchFailure, request, failure); - } - - private void notifyAfterDispatch(Request request) - { - notifyEvent1(listener -> listener::onAfterDispatch, request); - } - - private void notifyRequestContent(Request request, ByteBuffer content) - { - notifyEvent2(listener -> listener::onRequestContent, request, content); - } - - private void notifyRequestContentEnd(Request request) - { - notifyEvent1(listener -> listener::onRequestContentEnd, request); - } - - private void notifyRequestTrailers(Request request) - { - notifyEvent1(listener -> listener::onRequestTrailers, request); - } - - private void notifyRequestEnd(Request request) - { - notifyEvent1(listener -> listener::onRequestEnd, request); - } - - private void notifyRequestFailure(Request request, Throwable failure) - { - notifyEvent2(listener -> listener::onRequestFailure, request, failure); - } - - private void notifyResponseBegin(Request request) - { - notifyEvent1(listener -> listener::onResponseBegin, request); - } - - private void notifyResponseCommit(Request request) - { - notifyEvent1(listener -> listener::onResponseCommit, request); - } - - private void notifyResponseContent(Request request, ByteBuffer content) - { - notifyEvent2(listener -> listener::onResponseContent, request, content); - } - - private void notifyResponseEnd(Request request) - { - notifyEvent1(listener -> listener::onResponseEnd, request); - } - - private void notifyResponseFailure(Request request, Throwable failure) - { - notifyEvent2(listener -> listener::onResponseFailure, request, failure); - } - - private void notifyComplete(Request request) - { - notifyEvent1(listener -> listener::onComplete, request); - } - private void notifyEvent1(Function<Listener, Consumer<Request>> function, Request request) { - for (Listener listener : _listeners) + for (Listener listener : _transientListeners) { try { @@ -1033,7 +979,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor private void notifyEvent2(Function<Listener, BiConsumer<Request, ByteBuffer>> function, Request request, ByteBuffer content) { - for (Listener listener : _listeners) + for (Listener listener : _transientListeners) { ByteBuffer view = content.slice(); try @@ -1049,7 +995,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor private void notifyEvent2(Function<Listener, BiConsumer<Request, Throwable>> function, Request request, Throwable failure) { - for (Listener listener : _listeners) + for (Listener listener : _transientListeners) { try { @@ -1085,8 +1031,13 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * <p>Listener methods are invoked synchronously from the thread that is * performing the request processing, and they should not call blocking code * (otherwise the request processing will be blocked as well).</p> + * <p>Listener instances that are set as a bean on the {@link Connector} are + * efficiently added to {@link HttpChannel}. If additional listeners are added + * using the deprecated {@link HttpChannel#addListener(Listener)}</p> method, + * then an instance of {@link TransientListeners} must be added to the connector + * in order for them to be invoked. */ - public interface Listener + public interface Listener extends EventListener { /** * Invoked just after the HTTP request line and headers have been parsed. @@ -1256,11 +1207,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor _response.getHttpOutput().closed(); super.succeeded(); if (_commit) - notifyResponseCommit(_request); + _combinedListener.onResponseCommit(_request); if (_length > 0) - notifyResponseContent(_request, _content); + _combinedListener.onResponseContent(_request, _content); if (_complete && _state.completeResponse()) - notifyResponseEnd(_request); + _combinedListener.onResponseEnd(_request); } @Override @@ -1313,4 +1264,102 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor super.failed(new IllegalStateException()); } } + + /** + * A Listener instance that can be added as a bean to {@link AbstractConnector} so that + * the listeners obtained from HttpChannel{@link #getTransientListeners()} + */ + @Deprecated + public static class TransientListeners implements Listener + { + @Override + public void onRequestBegin(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onRequestBegin, request); + } + + @Override + public void onBeforeDispatch(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onBeforeDispatch, request); + } + + @Override + public void onDispatchFailure(Request request, Throwable failure) + { + request.getHttpChannel().notifyEvent2(listener -> listener::onDispatchFailure, request, failure); + } + + @Override + public void onAfterDispatch(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onAfterDispatch, request); + } + + @Override + public void onRequestContent(Request request, ByteBuffer content) + { + request.getHttpChannel().notifyEvent2(listener -> listener::onRequestContent, request, content); + } + + @Override + public void onRequestContentEnd(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onRequestContentEnd, request); + } + + @Override + public void onRequestTrailers(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onRequestTrailers, request); + } + + @Override + public void onRequestEnd(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onRequestEnd, request); + } + + @Override + public void onRequestFailure(Request request, Throwable failure) + { + request.getHttpChannel().notifyEvent2(listener -> listener::onRequestFailure, request, failure); + } + + @Override + public void onResponseBegin(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onResponseBegin, request); + } + + @Override + public void onResponseCommit(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onResponseCommit, request); + } + + @Override + public void onResponseContent(Request request, ByteBuffer content) + { + request.getHttpChannel().notifyEvent2(listener -> listener::onResponseContent, request, content); + } + + @Override + public void onResponseEnd(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onResponseEnd, request); + } + + @Override + public void onResponseFailure(Request request, Throwable failure) + { + request.getHttpChannel().notifyEvent2(listener -> listener::onResponseFailure, request, failure); + } + + @Override + public void onComplete(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onComplete, request); + } + } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelListeners.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelListeners.java new file mode 100644 index 00000000000..281c7f5eb04 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelListeners.java @@ -0,0 +1,286 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.server; + +import java.nio.ByteBuffer; +import java.util.Collection; + +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/** + * A {@link HttpChannel.Listener} that holds a collection of + * other {@link HttpChannel.Listener} instances that are efficiently + * invoked without iteration. + * @see AbstractConnector + */ +public class HttpChannelListeners implements HttpChannel.Listener +{ + static final Logger LOG = Log.getLogger(HttpChannel.class); + public static HttpChannel.Listener NOOP = new HttpChannel.Listener() {}; + + private final NotifyRequest onRequestBegin; + private final NotifyRequest onBeforeDispatch; + private final NotifyFailure onDispatchFailure; + private final NotifyRequest onAfterDispatch; + private final NotifyContent onRequestContent; + private final NotifyRequest onRequestContentEnd; + private final NotifyRequest onRequestTrailers; + private final NotifyRequest onRequestEnd; + private final NotifyFailure onRequestFailure; + private final NotifyRequest onResponseBegin; + private final NotifyRequest onResponseCommit; + private final NotifyContent onResponseContent; + private final NotifyRequest onResponseEnd; + private final NotifyFailure onResponseFailure; + private final NotifyRequest onComplete; + + public HttpChannelListeners(Collection<HttpChannel.Listener> listeners) + { + try + { + NotifyRequest onRequestBegin = NotifyRequest.NOOP; + NotifyRequest onBeforeDispatch = NotifyRequest.NOOP; + NotifyFailure onDispatchFailure = NotifyFailure.NOOP; + NotifyRequest onAfterDispatch = NotifyRequest.NOOP; + NotifyContent onRequestContent = NotifyContent.NOOP; + NotifyRequest onRequestContentEnd = NotifyRequest.NOOP; + NotifyRequest onRequestTrailers = NotifyRequest.NOOP; + NotifyRequest onRequestEnd = NotifyRequest.NOOP; + NotifyFailure onRequestFailure = NotifyFailure.NOOP; + NotifyRequest onResponseBegin = NotifyRequest.NOOP; + NotifyRequest onResponseCommit = NotifyRequest.NOOP; + NotifyContent onResponseContent = NotifyContent.NOOP; + NotifyRequest onResponseEnd = NotifyRequest.NOOP; + NotifyFailure onResponseFailure = NotifyFailure.NOOP; + NotifyRequest onComplete = NotifyRequest.NOOP; + + for (HttpChannel.Listener listener : listeners) + { + if (!listener.getClass().getMethod("onRequestBegin", Request.class).isDefault()) + onRequestBegin = combine(onRequestBegin, listener::onRequestBegin); + if (!listener.getClass().getMethod("onBeforeDispatch", Request.class).isDefault()) + onBeforeDispatch = combine(onBeforeDispatch, listener::onBeforeDispatch); + if (!listener.getClass().getMethod("onDispatchFailure", Request.class, Throwable.class).isDefault()) + onDispatchFailure = combine(onDispatchFailure, listener::onDispatchFailure); + if (!listener.getClass().getMethod("onAfterDispatch", Request.class).isDefault()) + onAfterDispatch = combine(onAfterDispatch, listener::onAfterDispatch); + if (!listener.getClass().getMethod("onRequestContent", Request.class, ByteBuffer.class).isDefault()) + onRequestContent = combine(onRequestContent, listener::onRequestContent); + if (!listener.getClass().getMethod("onRequestContentEnd", Request.class).isDefault()) + onRequestContentEnd = combine(onRequestContentEnd, listener::onRequestContentEnd); + if (!listener.getClass().getMethod("onRequestTrailers", Request.class).isDefault()) + onRequestTrailers = combine(onRequestTrailers, listener::onRequestTrailers); + if (!listener.getClass().getMethod("onRequestEnd", Request.class).isDefault()) + onRequestEnd = combine(onRequestEnd, listener::onRequestEnd); + if (!listener.getClass().getMethod("onRequestFailure", Request.class, Throwable.class).isDefault()) + onRequestFailure = combine(onRequestFailure, listener::onRequestFailure); + if (!listener.getClass().getMethod("onResponseBegin", Request.class).isDefault()) + onResponseBegin = combine(onResponseBegin, listener::onResponseBegin); + if (!listener.getClass().getMethod("onResponseCommit", Request.class).isDefault()) + onResponseCommit = combine(onResponseCommit, listener::onResponseCommit); + if (!listener.getClass().getMethod("onResponseContent", Request.class, ByteBuffer.class).isDefault()) + onResponseContent = combine(onResponseContent, listener::onResponseContent); + if (!listener.getClass().getMethod("onResponseEnd", Request.class).isDefault()) + onResponseEnd = combine(onResponseEnd, listener::onResponseEnd); + if (!listener.getClass().getMethod("onResponseFailure", Request.class, Throwable.class).isDefault()) + onResponseFailure = combine(onResponseFailure, listener::onResponseFailure); + if (!listener.getClass().getMethod("onComplete", Request.class).isDefault()) + onComplete = combine(onComplete, listener::onComplete); + } + + this.onRequestBegin = onRequestBegin; + this.onBeforeDispatch = onBeforeDispatch; + this.onDispatchFailure = onDispatchFailure; + this.onAfterDispatch = onAfterDispatch; + this.onRequestContent = onRequestContent; + this.onRequestContentEnd = onRequestContentEnd; + this.onRequestTrailers = onRequestTrailers; + this.onRequestEnd = onRequestEnd; + this.onRequestFailure = onRequestFailure; + this.onResponseBegin = onResponseBegin; + this.onResponseCommit = onResponseCommit; + this.onResponseContent = onResponseContent; + this.onResponseEnd = onResponseEnd; + this.onResponseFailure = onResponseFailure; + this.onComplete = onComplete; + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + + @Override + public void onRequestBegin(Request request) + { + onRequestBegin.onRequest(request); + } + + @Override + public void onBeforeDispatch(Request request) + { + onBeforeDispatch.onRequest(request); + } + + @Override + public void onDispatchFailure(Request request, Throwable failure) + { + onDispatchFailure.onFailure(request, failure); + } + + @Override + public void onAfterDispatch(Request request) + { + onAfterDispatch.onRequest(request); + } + + @Override + public void onRequestContent(Request request, ByteBuffer content) + { + onRequestContent.onContent(request, content); + } + + @Override + public void onRequestContentEnd(Request request) + { + onRequestContentEnd.onRequest(request); + } + + @Override + public void onRequestTrailers(Request request) + { + onRequestTrailers.onRequest(request); + } + + @Override + public void onRequestEnd(Request request) + { + onRequestEnd.onRequest(request); + } + + @Override + public void onRequestFailure(Request request, Throwable failure) + { + onRequestFailure.onFailure(request, failure); + } + + @Override + public void onResponseBegin(Request request) + { + onResponseBegin.onRequest(request); + } + + @Override + public void onResponseCommit(Request request) + { + onResponseCommit.onRequest(request); + } + + @Override + public void onResponseContent(Request request, ByteBuffer content) + { + onResponseContent.onContent(request, content); + } + + @Override + public void onResponseEnd(Request request) + { + onResponseEnd.onRequest(request); + } + + @Override + public void onResponseFailure(Request request, Throwable failure) + { + onResponseFailure.onFailure(request, failure); + } + + @Override + public void onComplete(Request request) + { + onComplete.onRequest(request); + } + + private interface NotifyRequest + { + void onRequest(Request request); + + NotifyRequest NOOP = request -> + { + }; + } + + private interface NotifyFailure + { + void onFailure(Request request, Throwable failure); + + NotifyFailure NOOP = (request, failure) -> + { + }; + } + + private interface NotifyContent + { + void onContent(Request request, ByteBuffer content); + + NotifyContent NOOP = (request, content) -> + { + }; + } + + private static NotifyRequest combine(NotifyRequest first, NotifyRequest second) + { + if (first == NotifyRequest.NOOP) + return second; + if (second == NotifyRequest.NOOP) + return first; + return request -> + { + first.onRequest(request); + second.onRequest(request); + }; + } + + private static NotifyFailure combine(NotifyFailure first, NotifyFailure second) + { + if (first == NotifyFailure.NOOP) + return second; + if (second == NotifyFailure.NOOP) + return first; + return (request, throwable) -> + { + first.onFailure(request, throwable); + second.onFailure(request, throwable); + }; + } + + private static NotifyContent combine(NotifyContent first, NotifyContent second) + { + if (first == NotifyContent.NOOP) + return (request, content) -> second.onContent(request, content.slice()); + if (second == NotifyContent.NOOP) + return (request, content) -> first.onContent(request, content.slice()); + return (request, content) -> + { + content = content.slice(); + first.onContent(request, content); + second.onContent(request, content); + }; + } +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java index a57a550d80b..ef94f6cbf80 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -1873,6 +1873,7 @@ public class Request implements HttpServletRequest _remote = null; _sessions = null; _input.recycle(); + _requestAttributeListeners.clear(); } /* diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpChannelEventTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpChannelEventTest.java index 15bbfaf9d36..516a81aeeb2 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpChannelEventTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpChannelEventTest.java @@ -240,6 +240,37 @@ public class HttpChannelEventTest assertThat(elapsed.get(), Matchers.greaterThan(0L)); } + @Test + public void testTransientListener() throws Exception + { + start(new TestHandler()); + + CountDownLatch latch = new CountDownLatch(1); + connector.addBean(new HttpChannel.TransientListeners()); + connector.addBean(new HttpChannel.Listener() + { + @Override + public void onRequestBegin(Request request) + { + request.getHttpChannel().addListener(new HttpChannel.Listener() + { + @Override + public void onComplete(Request request) + { + latch.countDown(); + } + }); + } + }); + + HttpTester.Request request = HttpTester.newRequest(); + request.setHeader("Host", "localhost"); + HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request.toString(), 5, TimeUnit.SECONDS)); + + assertEquals(HttpStatus.OK_200, response.getStatus()); + assertTrue(latch.await(5, TimeUnit.SECONDS)); + } + private static class TestHandler extends AbstractHandler.ErrorDispatchHandler { @Override diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputAsyncStateTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputAsyncStateTest.java index fcabf1acfe9..a537317f7f9 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputAsyncStateTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputAsyncStateTest.java @@ -95,7 +95,7 @@ public class HttpInputAsyncStateTest public void before() { _noReadInDataAvailable = false; - _in = new HttpInput(new HttpChannelState(new HttpChannel(null, new HttpConfiguration(), null, null) + _in = new HttpInput(new HttpChannelState(new HttpChannel(new MockConnector(), new HttpConfiguration(), null, null) { @Override public void onAsyncWaitForContent() diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputTest.java index c2b43ed0954..fa9fa1a25df 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputTest.java @@ -92,7 +92,7 @@ public class HttpInputTest @BeforeEach public void before() { - _in = new HttpInput(new HttpChannelState(new HttpChannel(null, new HttpConfiguration(), null, null) + _in = new HttpInput(new HttpChannelState(new HttpChannel(new MockConnector(), new HttpConfiguration(), null, null) { @Override public void onAsyncWaitForContent() diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpWriterTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpWriterTest.java index 69a3dd171bb..c2a54f8de30 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpWriterTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpWriterTest.java @@ -45,7 +45,7 @@ public class HttpWriterTest final ByteBufferPool pool = new ArrayByteBufferPool(); - HttpChannel channel = new HttpChannel(null, new HttpConfiguration(), null, null) + HttpChannel channel = new HttpChannel(new MockConnector(), new HttpConfiguration(), null, null) { @Override public ByteBufferPool getByteBufferPool() diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/MockConnector.java b/jetty-server/src/test/java/org/eclipse/jetty/server/MockConnector.java new file mode 100644 index 00000000000..847bc34b7f9 --- /dev/null +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/MockConnector.java @@ -0,0 +1,46 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.server; + +import java.io.IOException; + +class MockConnector extends AbstractConnector +{ + public MockConnector() + { + super(new Server() , null, null, null, 0); + } + + @Override + protected void accept(int acceptorID) throws IOException, InterruptedException + { + } + + @Override + public Object getTransport() + { + return null; + } + + @Override + public String dumpSelf() + { + return null; + } +} diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/component/Container.java b/jetty-util/src/main/java/org/eclipse/jetty/util/component/Container.java index b195e150095..51a75156767 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/component/Container.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/component/Container.java @@ -43,7 +43,7 @@ public interface Container /** * @param clazz the class of the beans * @param <T> the Bean type - * @return the list of beans of the given class (or subclass) + * @return a list of beans of the given class (or subclass) * @see #getBeans() * @see #getContainedBeans(Class) */ diff --git a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractClusteredInvalidationSessionTest.java b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractClusteredInvalidationSessionTest.java index ecce32321af..e0af754ec22 100644 --- a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractClusteredInvalidationSessionTest.java +++ b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractClusteredInvalidationSessionTest.java @@ -65,8 +65,8 @@ public abstract class AbstractClusteredInvalidationSessionTest extends AbstractT TestServer server1 = new TestServer(0, maxInactiveInterval, scavengeInterval, cacheFactory1, storeFactory1); ServletContextHandler context = server1.addContext(contextPath); context.addServlet(TestServlet.class, servletMapping); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - context.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + server1.getServerConnector().addBean(scopeListener); try { diff --git a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractClusteredSessionScavengingTest.java b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractClusteredSessionScavengingTest.java index d812834eb30..06bf2f6edc9 100644 --- a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractClusteredSessionScavengingTest.java +++ b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractClusteredSessionScavengingTest.java @@ -77,8 +77,8 @@ public abstract class AbstractClusteredSessionScavengingTest extends AbstractTes TestServlet servlet1 = new TestServlet(); ServletHolder holder1 = new ServletHolder(servlet1); ServletContextHandler context = server1.addContext(contextPath); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - context.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + server1.getServerConnector().addBean(scopeListener); TestSessionListener listener1 = new TestSessionListener(); context.getSessionHandler().addEventListener(listener1); context.addServlet(holder1, servletMapping); @@ -96,8 +96,8 @@ public abstract class AbstractClusteredSessionScavengingTest extends AbstractTes ((AbstractSessionDataStoreFactory)storeFactory2).setSavePeriodSec(0); //always save when the session exits TestServer server2 = new TestServer(0, maxInactivePeriod, scavengePeriod, cacheFactory2, storeFactory2); ServletContextHandler context2 = server2.addContext(contextPath); - TestContextScopeListener scopeListener2 = new TestContextScopeListener(); - context2.addEventListener(scopeListener2); + TestHttpChannelCompleteListener scopeListener2 = new TestHttpChannelCompleteListener(); + server2.getServerConnector().addBean(scopeListener2); context2.addServlet(TestServlet.class, servletMapping); SessionHandler m2 = context2.getSessionHandler(); diff --git a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractWebAppObjectInSessionTest.java b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractWebAppObjectInSessionTest.java index 967085da2f9..8fcc6bfca1e 100644 --- a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractWebAppObjectInSessionTest.java +++ b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractWebAppObjectInSessionTest.java @@ -23,7 +23,6 @@ import java.io.FileOutputStream; import java.io.FileWriter; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; - import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.client.HttpClient; @@ -103,8 +102,8 @@ public abstract class AbstractWebAppObjectInSessionTest extends AbstractTestBase TestServer server1 = new TestServer(0, TestServer.DEFAULT_MAX_INACTIVE, TestServer.DEFAULT_SCAVENGE_SEC, cacheFactory, storeFactory); WebAppContext wac1 = server1.addWebAppContext(warDir.getCanonicalPath(), contextPath); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - wac1.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + server1.getServerConnector().addBean(scopeListener); wac1.addServlet(WebAppObjectInSessionServlet.class.getName(), servletMapping); try diff --git a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/TestContextScopeListener.java b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/TestContextScopeListener.java deleted file mode 100644 index 99f4d0e13a2..00000000000 --- a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/TestContextScopeListener.java +++ /dev/null @@ -1,76 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. -// ------------------------------------------------------------------------ -// All rights reserved. This program and the accompanying materials -// are made available under the terms of the Eclipse Public License v1.0 -// and Apache License v2.0 which accompanies this distribution. -// -// The Eclipse Public License is available at -// http://www.eclipse.org/legal/epl-v10.html -// -// The Apache License v2.0 is available at -// http://www.opensource.org/licenses/apache2.0.php -// -// You may elect to redistribute this code under either of these licenses. -// ======================================================================== -// - -package org.eclipse.jetty.server.session; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicReference; - -import org.eclipse.jetty.server.HttpChannel.Listener; -import org.eclipse.jetty.server.HttpChannelState; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.handler.ContextHandler.Context; -import org.eclipse.jetty.server.handler.ContextHandler.ContextScopeListener; - -public class TestContextScopeListener implements ContextScopeListener -{ - AtomicReference<CountDownLatch> _exitSynchronizer = new AtomicReference<>(); - boolean listenerAdded = false; - - /** - * @return the exitSynchronizer - */ - public CountDownLatch getExitSynchronizer() - { - return _exitSynchronizer.get(); - } - - /** - * @param exitSynchronizer the exitSynchronizer to set - */ - public void setExitSynchronizer(CountDownLatch exitSynchronizer) - { - _exitSynchronizer.set(exitSynchronizer); - } - - @Override - public void enterScope(Context context, org.eclipse.jetty.server.Request request, Object reason) - { - //noop - } - - @Override - public void exitScope(final Context context, final org.eclipse.jetty.server.Request request) - { - if (request != null && !listenerAdded) - { - listenerAdded = true; - request.getHttpChannel().addListener(new Listener() - { - @Override - public void onComplete(Request request) - { - Listener.super.onComplete(request); - if (_exitSynchronizer.get() != null) - _exitSynchronizer.get().countDown(); - } - - }); - } - } -} diff --git a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/TestHttpChannelCompleteListener.java b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/TestHttpChannelCompleteListener.java new file mode 100644 index 00000000000..7fb22f00007 --- /dev/null +++ b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/TestHttpChannelCompleteListener.java @@ -0,0 +1,45 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.server.session; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.jetty.server.HttpChannel.Listener; +import org.eclipse.jetty.server.Request; + +public class TestHttpChannelCompleteListener implements Listener +{ + private final AtomicReference<CountDownLatch> _exitSynchronizer = new AtomicReference<>(); + + /** + * @param exitSynchronizer the exitSynchronizer to set + */ + public void setExitSynchronizer(CountDownLatch exitSynchronizer) + { + _exitSynchronizer.set(exitSynchronizer); + } + + @Override + public void onComplete(Request request) + { + if (_exitSynchronizer.get() != null) + _exitSynchronizer.get().countDown(); + } +} diff --git a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/TestServer.java b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/TestServer.java index 85f3d5dd341..f66bdfc1c5a 100644 --- a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/TestServer.java +++ b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/TestServer.java @@ -20,6 +20,7 @@ package org.eclipse.jetty.server.session; import org.eclipse.jetty.server.NetworkConnector; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SessionIdManager; import org.eclipse.jetty.server.handler.ContextHandlerCollection; import org.eclipse.jetty.servlet.ServletContextHandler; @@ -93,6 +94,11 @@ public class TestServer return h; } + public ServerConnector getServerConnector() + { + return _server.getBean(ServerConnector.class); + } + public void start() throws Exception { // server -> contexts collection -> context handler -> session handler -> servlet handler diff --git a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/AsyncTest.java b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/AsyncTest.java index 4a0f031b2f6..8d2ae14a05b 100644 --- a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/AsyncTest.java +++ b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/AsyncTest.java @@ -22,7 +22,6 @@ import java.io.IOException; import java.lang.reflect.Proxy; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; - import javax.servlet.AsyncContext; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; @@ -65,8 +64,8 @@ public class AsyncTest String mapping = "/server"; ServletContextHandler contextHandler = server.addContext(contextPath); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - contextHandler.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + server.getServerConnector().addBean(scopeListener); TestServlet servlet = new TestServlet(); ServletHolder holder = new ServletHolder(servlet); contextHandler.addServlet(holder, mapping); @@ -117,8 +116,8 @@ public class AsyncTest String mapping = "/server"; ServletContextHandler contextHandler = server.addContext(contextPath); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - contextHandler.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + server.getServerConnector().addBean(scopeListener); TestServlet servlet = new TestServlet(); ServletHolder holder = new ServletHolder(servlet); contextHandler.addServlet(holder, mapping); @@ -167,8 +166,8 @@ public class AsyncTest TestServer server = new TestServer(0, -1, -1, cacheFactory, storeFactory); ServletContextHandler contextA = server.addContext("/ctxA"); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - contextA.addEventListener(scopeListener); //just pick one of the contexts to register the listener + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + server.getServerConnector().addBean(scopeListener); //just pick one of the contexts to register the listener CrossContextServlet ccServlet = new CrossContextServlet(); ServletHolder ccHolder = new ServletHolder(ccServlet); contextA.addServlet(ccHolder, "/*"); @@ -224,8 +223,8 @@ public class AsyncTest String mapping = "/server"; ServletContextHandler contextHandler = server.addContext(contextPath); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - contextHandler.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + server.getServerConnector().addBean(scopeListener); TestServlet servlet = new TestServlet(); ServletHolder holder = new ServletHolder(servlet); @@ -277,8 +276,8 @@ public class AsyncTest TestServer server = new TestServer(0, -1, -1, cacheFactory, storeFactory); ServletContextHandler contextA = server.addContext("/ctxA"); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - contextA.addEventListener(scopeListener); //just pick a context + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + server.getServerConnector().addBean(scopeListener); CrossContextServlet ccServlet = new CrossContextServlet(); ServletHolder ccHolder = new ServletHolder(ccServlet); contextA.addServlet(ccHolder, "/*"); diff --git a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/CreationTest.java b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/CreationTest.java index 7c8e8872d56..d2891111e43 100644 --- a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/CreationTest.java +++ b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/CreationTest.java @@ -76,8 +76,8 @@ public class CreationTest TestServlet servlet = new TestServlet(); ServletHolder holder = new ServletHolder(servlet); ServletContextHandler contextHandler = server1.addContext(contextPath); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - contextHandler.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + server1.getServerConnector().addBean(scopeListener); contextHandler.addServlet(holder, servletMapping); servlet.setStore(contextHandler.getSessionHandler().getSessionCache().getSessionDataStore()); server1.start(); @@ -145,8 +145,8 @@ public class CreationTest TestServlet servlet = new TestServlet(); ServletHolder holder = new ServletHolder(servlet); ServletContextHandler contextHandler = server1.addContext(contextPath); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - contextHandler.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + server1.getServerConnector().addBean(scopeListener); contextHandler.addServlet(holder, servletMapping); servlet.setStore(contextHandler.getSessionHandler().getSessionCache().getSessionDataStore()); server1.start(); @@ -197,8 +197,8 @@ public class CreationTest TestServlet servlet = new TestServlet(); ServletHolder holder = new ServletHolder(servlet); ServletContextHandler contextHandler = server1.addContext(contextPath); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - contextHandler.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + server1.getServerConnector().addBean(scopeListener); contextHandler.addServlet(holder, servletMapping); servlet.setStore(contextHandler.getSessionHandler().getSessionCache().getSessionDataStore()); server1.start(); @@ -245,8 +245,8 @@ public class CreationTest TestServlet servlet = new TestServlet(); ServletHolder holder = new ServletHolder(servlet); ServletContextHandler contextHandler = server1.addContext(contextPath); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - contextHandler.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + server1.getServerConnector().addBean(scopeListener); contextHandler.addServlet(holder, servletMapping); servlet.setStore(contextHandler.getSessionHandler().getSessionCache().getSessionDataStore()); server1.start(); @@ -299,8 +299,8 @@ public class CreationTest TestServlet servlet = new TestServlet(); ServletHolder holder = new ServletHolder(servlet); ServletContextHandler contextHandler = server1.addContext(contextPath); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - contextHandler.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + server1.getServerConnector().addBean(scopeListener); contextHandler.addServlet(holder, servletMapping); ServletContextHandler ctxB = server1.addContext(contextB); ctxB.addServlet(TestServletB.class, servletMapping); @@ -354,8 +354,8 @@ public class CreationTest TestServlet servlet = new TestServlet(); ServletHolder holder = new ServletHolder(servlet); ServletContextHandler contextHandler = server1.addContext(contextPath); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - contextHandler.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + server1.getServerConnector().addBean(scopeListener); contextHandler.addServlet(holder, servletMapping); ServletContextHandler ctxB = server1.addContext(contextB); ctxB.addServlet(TestServletB.class, servletMapping); diff --git a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/DefaultSessionCacheTest.java b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/DefaultSessionCacheTest.java index 87182acca2f..21b9bc44b83 100644 --- a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/DefaultSessionCacheTest.java +++ b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/DefaultSessionCacheTest.java @@ -22,7 +22,6 @@ import java.util.Collections; import java.util.Random; import java.util.Set; import java.util.concurrent.TimeUnit; - import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSessionActivationListener; import javax.servlet.http.HttpSessionEvent; @@ -79,8 +78,8 @@ public class DefaultSessionCacheTest SessionDataStoreFactory storeFactory = new TestSessionDataStoreFactory(); TestServer server = new TestServer(0, inactivePeriod, scavengePeriod, cacheFactory, storeFactory); ServletContextHandler contextHandler = server.addContext("/test"); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - contextHandler.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + server.getServerConnector().addBean(scopeListener); TestHttpSessionListener listener = new TestHttpSessionListener(); contextHandler.getSessionHandler().addEventListener(listener); diff --git a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/DeleteUnloadableSessionTest.java b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/DeleteUnloadableSessionTest.java index 6913d87acc3..2ede7430b8b 100644 --- a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/DeleteUnloadableSessionTest.java +++ b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/DeleteUnloadableSessionTest.java @@ -151,8 +151,8 @@ public class DeleteUnloadableSessionTest TestServer server = new TestServer(0, inactivePeriod, scavengePeriod, cacheFactory, storeFactory); ServletContextHandler context = server.addContext(contextPath); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - context.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + server.getServerConnector().addBean(scopeListener); TestServlet servlet = new TestServlet(); ServletHolder holder = new ServletHolder(servlet); diff --git a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/DirtyAttributeTest.java b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/DirtyAttributeTest.java index 640c37801bd..92505dcfa75 100644 --- a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/DirtyAttributeTest.java +++ b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/DirtyAttributeTest.java @@ -94,8 +94,8 @@ public class DirtyAttributeTest ServletContextHandler ctxA = server.addContext("/mod"); ctxA.addServlet(TestDirtyServlet.class, "/test"); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - ctxA.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + server.getServerConnector().addBean(scopeListener); server.start(); int port = server.getPort(); @@ -125,14 +125,12 @@ public class DirtyAttributeTest assertEquals(HttpServletResponse.SC_OK, response.getStatus()); //ensure request fully finished processing - latch.await(5, TimeUnit.SECONDS); + assertTrue(latch.await(5, TimeUnit.SECONDS)); A_VALUE.assertPassivatesEquals(1); A_VALUE.assertActivatesEquals(1); A_VALUE.assertBindsEquals(1); A_VALUE.assertUnbindsEquals(0); - - //do another request using the cookie to try changing the session attribute to the same value again latch = new CountDownLatch(1); diff --git a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/IdleSessionTest.java b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/IdleSessionTest.java index a11efa8436a..113e4a73aa3 100644 --- a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/IdleSessionTest.java +++ b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/IdleSessionTest.java @@ -76,8 +76,8 @@ public class IdleSessionTest _server1 = new TestServer(0, inactivePeriod, scavengePeriod, cacheFactory, storeFactory); ServletHolder holder = new ServletHolder(_servlet); ServletContextHandler contextHandler = _server1.addContext(contextPath); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - contextHandler.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + _server1.getServerConnector().addBean(scopeListener); contextHandler.addServlet(holder, servletMapping); _server1.start(); int port1 = _server1.getPort(); @@ -202,8 +202,8 @@ public class IdleSessionTest _server1 = new TestServer(0, inactivePeriod, scavengePeriod, cacheFactory, storeFactory); ServletHolder holder = new ServletHolder(_servlet); ServletContextHandler contextHandler = _server1.addContext(contextPath); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - contextHandler.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + _server1.getServerConnector().addBean(scopeListener); contextHandler.addServlet(holder, servletMapping); _server1.start(); int port1 = _server1.getPort(); diff --git a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/NonClusteredSessionScavengingTest.java b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/NonClusteredSessionScavengingTest.java index 07c3b8f284c..0c7d6b43dc7 100644 --- a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/NonClusteredSessionScavengingTest.java +++ b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/NonClusteredSessionScavengingTest.java @@ -75,8 +75,8 @@ public class NonClusteredSessionScavengingTest extends AbstractTestBase context1.addServlet(TestServlet.class, servletMapping); TestHttpSessionListener listener = new TestHttpSessionListener(); context1.getSessionHandler().addEventListener(listener); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - context1.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + server1.getServerConnector().addBean(scopeListener); try { @@ -144,8 +144,8 @@ public class NonClusteredSessionScavengingTest extends AbstractTestBase TestServer server = new TestServer(0, maxInactivePeriod, scavengePeriod, cacheFactory, storeFactory); ServletContextHandler context = server.addContext("/"); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - context.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + server.getServerConnector().addBean(scopeListener); _dataStore = context.getSessionHandler().getSessionCache().getSessionDataStore(); context.addServlet(TestServlet.class, servletMapping); @@ -208,8 +208,8 @@ public class NonClusteredSessionScavengingTest extends AbstractTestBase TestServer server = new TestServer(0, maxInactivePeriod, scavengePeriod, cacheFactory, storeFactory); ServletContextHandler context = server.addContext("/"); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - context.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + server.getServerConnector().addBean(scopeListener); _dataStore = context.getSessionHandler().getSessionCache().getSessionDataStore(); context.addServlet(TestServlet.class, servletMapping); String contextPath = "/"; diff --git a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/ReentrantRequestSessionTest.java b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/ReentrantRequestSessionTest.java index 731a7c9ad37..663444b8a82 100644 --- a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/ReentrantRequestSessionTest.java +++ b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/ReentrantRequestSessionTest.java @@ -59,8 +59,8 @@ public class ReentrantRequestSessionTest ServletContextHandler context = server.addContext(contextPath); context.addServlet(TestServlet.class, servletMapping); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - context.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + server.getServerConnector().addBean(scopeListener); try { diff --git a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/SameContextForwardedSessionTest.java b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/SameContextForwardedSessionTest.java index 368283c365f..2fe9d7b4469 100644 --- a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/SameContextForwardedSessionTest.java +++ b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/SameContextForwardedSessionTest.java @@ -58,8 +58,8 @@ public class SameContextForwardedSessionTest TestServer testServer = new TestServer(0, -1, -1, cacheFactory, storeFactory); ServletContextHandler testServletContextHandler = testServer.addContext("/context"); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - testServletContextHandler.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + testServer.getServerConnector().addBean(scopeListener); ServletHolder holder = new ServletHolder(new Servlet1()); testServletContextHandler.addServlet(holder, "/one"); testServletContextHandler.addServlet(Servlet2.class, "/two"); diff --git a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/SaveOptimizeTest.java b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/SaveOptimizeTest.java index a036bd0bd5c..4acc49516aa 100644 --- a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/SaveOptimizeTest.java +++ b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/SaveOptimizeTest.java @@ -124,8 +124,8 @@ public class SaveOptimizeTest _servlet = new TestServlet(); ServletHolder holder = new ServletHolder(_servlet); ServletContextHandler contextHandler = _server1.addContext(contextPath); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - contextHandler.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + _server1.getServerConnector().addBean(scopeListener); contextHandler.addServlet(holder, servletMapping); _servlet.setStore(contextHandler.getSessionHandler().getSessionCache().getSessionDataStore()); _server1.start(); @@ -207,8 +207,8 @@ public class SaveOptimizeTest _servlet = new TestServlet(); ServletHolder holder = new ServletHolder(_servlet); ServletContextHandler contextHandler = _server1.addContext(contextPath); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - contextHandler.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + _server1.getServerConnector().addBean(scopeListener); contextHandler.addServlet(holder, servletMapping); _servlet.setStore(contextHandler.getSessionHandler().getSessionCache().getSessionDataStore()); _server1.start(); @@ -297,8 +297,8 @@ public class SaveOptimizeTest _servlet = new TestServlet(); ServletHolder holder = new ServletHolder(_servlet); ServletContextHandler contextHandler = _server1.addContext(contextPath); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - contextHandler.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + _server1.getServerConnector().addBean(scopeListener); contextHandler.addServlet(holder, servletMapping); _servlet.setStore(contextHandler.getSessionHandler().getSessionCache().getSessionDataStore()); _server1.start(); @@ -392,8 +392,8 @@ public class SaveOptimizeTest _servlet = new TestServlet(); ServletHolder holder = new ServletHolder(_servlet); ServletContextHandler contextHandler = _server1.addContext(contextPath); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - contextHandler.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + _server1.getServerConnector().addBean(scopeListener); contextHandler.addServlet(holder, servletMapping); _servlet.setStore(contextHandler.getSessionHandler().getSessionCache().getSessionDataStore()); _server1.start(); @@ -471,8 +471,8 @@ public class SaveOptimizeTest _servlet = new TestServlet(); ServletHolder holder = new ServletHolder(_servlet); ServletContextHandler contextHandler = _server1.addContext(contextPath); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - contextHandler.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + _server1.getServerConnector().addBean(scopeListener); contextHandler.addServlet(holder, servletMapping); _servlet.setStore(contextHandler.getSessionHandler().getSessionCache().getSessionDataStore()); _server1.start(); diff --git a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/SessionRenewTest.java b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/SessionRenewTest.java index 828991aee94..69b8b5ec813 100644 --- a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/SessionRenewTest.java +++ b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/SessionRenewTest.java @@ -22,7 +22,6 @@ import java.io.IOException; import java.net.HttpCookie; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; - import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -41,7 +40,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotSame; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -118,8 +116,8 @@ public class SessionRenewTest String contextPathA = ""; String servletMapping = "/server"; WebAppContext contextA = _server.addWebAppContext(".", contextPathA); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - contextA.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + _server.getServerConnector().addBean(scopeListener); contextA.setParentLoaderPriority(true); contextA.addServlet(TestServlet.class, servletMapping); @@ -185,8 +183,8 @@ public class SessionRenewTest String contextPath = ""; String servletMapping = "/server"; WebAppContext context = _server.addWebAppContext(".", contextPath); - TestContextScopeListener scopeListener = new TestContextScopeListener(); - context.addEventListener(scopeListener); + TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener(); + _server.getServerConnector().addBean(scopeListener); context.setParentLoaderPriority(true); context.addServlet(TestServlet.class, servletMapping); TestHttpSessionIdListener testListener = new TestHttpSessionIdListener(); From 2564a08150bc36e81c02a20fcadfe4b8f9bcf8b3 Mon Sep 17 00:00:00 2001 From: Greg Wilkins <gregw@webtide.com> Date: Tue, 27 Aug 2019 18:43:33 +1000 Subject: [PATCH 37/55] fix checkstyle Signed-off-by: Greg Wilkins <gregw@webtide.com> --- .../jetty/server/session/NullSessionCache.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCache.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCache.java index 6e88ab45a7c..e22919eed7b 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCache.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCache.java @@ -25,7 +25,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSessionAttributeListener; import javax.servlet.http.HttpSessionBindingEvent; - /** * NullSessionCache * @@ -75,8 +74,7 @@ public class NullSessionCache extends AbstractSessionCache if (store == null) return; - if (_writeThroughMode == WriteThroughMode.ALWAYS - || (_writeThroughMode == WriteThroughMode.NEW && session.isNew())) + if (_writeThroughMode == WriteThroughMode.ALWAYS || (_writeThroughMode == WriteThroughMode.NEW && session.isNew())) { //ensure that a call to willPassivate doesn't result in a passivation //listener removing an attribute, which would cause this listener to @@ -128,11 +126,10 @@ public class NullSessionCache extends AbstractSessionCache * (which is the default behaviour of AbstractSessionCache) */ ON_EXIT - }; + } private WriteThroughMode _writeThroughMode = WriteThroughMode.ON_EXIT; protected WriteThroughAttributeListener _listener = null; - /** * @return the writeThroughMode @@ -142,14 +139,13 @@ public class NullSessionCache extends AbstractSessionCache return _writeThroughMode; } - /** * @param writeThroughMode the writeThroughMode to set */ public void setWriteThroughMode(WriteThroughMode writeThroughMode) { if (getSessionHandler() == null) - throw new IllegalStateException ("No SessionHandler"); + throw new IllegalStateException("No SessionHandler"); //assume setting null is the same as ON_EXIT if (writeThroughMode == null) @@ -184,7 +180,6 @@ public class NullSessionCache extends AbstractSessionCache _writeThroughMode = writeThroughMode; } - /** * @param handler The SessionHandler related to this SessionCache */ @@ -193,7 +188,6 @@ public class NullSessionCache extends AbstractSessionCache super(handler); super.setEvictionPolicy(EVICT_ON_SESSION_EXIT); } - /** * @see org.eclipse.jetty.server.session.SessionCache#shutdown() From d1c462c966982a50e8647a69e82b526326cfa7ef Mon Sep 17 00:00:00 2001 From: Greg Wilkins <gregw@webtide.com> Date: Tue, 27 Aug 2019 19:06:32 +1000 Subject: [PATCH 38/55] fix checkstyle Signed-off-by: Greg Wilkins <gregw@webtide.com> --- .../src/main/java/org/eclipse/jetty/http/CookieCutter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCutter.java b/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCutter.java index 2b137337842..d0ca77ba9d4 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCutter.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCutter.java @@ -192,7 +192,7 @@ public abstract class CookieCutter // This is a new cookie, so add the completed last cookie if we have one if (cookieName != null) { - if(!reject) + if (!reject) { addCookie(cookieName, cookieValue, cookieDomain, cookiePath, cookieVersion, cookieComment); reject = false; From 4d8e8050bd1940e8e1913ec62f6c959a088ff128 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt <joakim.erdfelt@gmail.com> Date: Tue, 27 Aug 2019 08:48:02 -0500 Subject: [PATCH 39/55] Fixes #4007 - using `user.home` system property instead of $HOME Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com> --- .../src/main/java/org/eclipse/jetty/start/StartArgs.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java b/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java index 028f5d7fdd3..09cf73b011d 100644 --- a/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java +++ b/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java @@ -842,7 +842,7 @@ public class StartArgs if (Utils.isBlank(localRepo)) { // Try generic env variable - Path home = Paths.get(System.getenv("HOME")); + Path home = Paths.get(System.getProperty("user.home")); Path localMavenRepository = home.resolve(".m2/repository"); if (Files.exists(localMavenRepository)) localRepo = localMavenRepository.toString(); From 6bcfa2dc6ef409ce0ef55b81d7765f63e4cd5b86 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt <joakim.erdfelt@gmail.com> Date: Tue, 27 Aug 2019 13:25:08 -0500 Subject: [PATCH 40/55] Fixes #4020 - Deprecate ExtensionFactory + This class is removed in Jetty 10 anyway. + If all you want is to access available extension names then use the Factory.getAvailableExtensionNames() method (which exists in Jetty 10.0.0 as well) Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com> --- .../api/extensions/ExtensionFactory.java | 6 ++++++ .../server/WebSocketServerFactory.java | 8 ++++++++ .../servlet/WebSocketServletFactory.java | 17 +++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/ExtensionFactory.java b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/ExtensionFactory.java index 8d46d60a5eb..94a27769457 100644 --- a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/ExtensionFactory.java +++ b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/ExtensionFactory.java @@ -24,6 +24,12 @@ import java.util.Map; import java.util.ServiceLoader; import java.util.Set; +/** + * The Factory for Extensions. + * + * @deprecated this class is removed from Jetty 10.0.0+ + */ +@Deprecated public abstract class ExtensionFactory implements Iterable<Class<? extends Extension>> { private ServiceLoader<Extension> extensionLoader = ServiceLoader.load(Extension.class); diff --git a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketServerFactory.java b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketServerFactory.java index 520302d95f5..2b5e116bd08 100644 --- a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketServerFactory.java +++ b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketServerFactory.java @@ -29,6 +29,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.concurrent.Executor; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; @@ -450,6 +451,13 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc return eventDriverFactory; } + @Override + public Set<String> getAvailableExtensionNames() + { + return Collections.unmodifiableSet(extensionFactory.getExtensionNames()); + } + + @Deprecated @Override public ExtensionFactory getExtensionFactory() { diff --git a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketServletFactory.java b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketServletFactory.java index fec0084cd30..b2d98b18ff6 100644 --- a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketServletFactory.java +++ b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketServletFactory.java @@ -21,6 +21,7 @@ package org.eclipse.jetty.websocket.servlet; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; +import java.util.Set; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -66,8 +67,24 @@ public interface WebSocketServletFactory void stop() throws Exception; + /** + * Get the set of available Extensions by registered name. + * + * @return the set of available extensions by registered name. + */ + Set<String> getAvailableExtensionNames(); + WebSocketCreator getCreator(); + /** + * Get the registered extensions for this WebSocket factory. + * + * @return the ExtensionFactory + * @see #getAvailableExtensionNames() + * @deprecated this class is removed from Jetty 10.0.0+. To remove specific extensions + * from negotiation use {@link WebSocketCreator} to remove then during handshake. + */ + @Deprecated ExtensionFactory getExtensionFactory(); /** From eef2481b59294b5783f3c4a3ea24f3262252c294 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt <joakim.erdfelt@gmail.com> Date: Tue, 27 Aug 2019 14:09:06 -0500 Subject: [PATCH 41/55] Adding cookie parsing test for excessive semicolon (reported as CVE in other projects) --- .../eclipse/jetty/server/CookieCutterTest.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutterTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutterTest.java index 04019ade121..77a7a74c24c 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutterTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutterTest.java @@ -18,6 +18,7 @@ package org.eclipse.jetty.server; +import java.util.Arrays; import javax.servlet.http.Cookie; import org.eclipse.jetty.http.CookieCompliance; @@ -259,4 +260,18 @@ public class CookieCutterTest assertCookie("Cookies[0]", cookies[0], "server.id", "abcd", 0, null); assertCookie("Cookies[1]", cookies[1], "server.detail", "cfg", 0, null); } + + @Test + public void testExcessiveSemicolons() + { + char[] excessive = new char[65535]; + Arrays.fill(excessive, ';'); + String rawCookie = "foo=bar; " + excessive + "; xyz=pdq"; + + Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie); + + assertThat("Cookies.length", cookies.length, is(2)); + assertCookie("Cookies[0]", cookies[0], "foo", "bar", 0, null); + assertCookie("Cookies[1]", cookies[1], "xyz", "pdq", 0, null); + } } From 8f383faf7866a6b417ff3241d69f8100f219b0b8 Mon Sep 17 00:00:00 2001 From: Greg Wilkins <gregw@webtide.com> Date: Wed, 28 Aug 2019 10:03:46 +1000 Subject: [PATCH 42/55] Issue #3872 Javax Websocket Packaging (#3873) * Issue #3872 Javax Websocket Packaging Moved JaxaxWebSocketConfiguration and SCI to config package. Limited classpath exposure in JavaxConfiguration (more needed) Updated tests with work around for those that needs more classes exposed Signed-off-by: Greg Wilkins <gregw@webtide.com> * Issue #3872 Javax Websocket Packaging Moved all remaining classes from org.eclipse.jetty.websocket.javax.server to org.eclipse.jetty.websocket.javax.server.internal. This works when running on the classpath, but the tests fail when running on the modulepath (eg in commandline mvn run). The issue appears to be that the tests don't load test classes from WEB-INF/lib or WEB-INF/classes. Instead the test classes were themselves in org.eclipse.jetty.websocket.javax.server, which is no longer exported from module-info.java. The hacked "fix" for this has been to create a org.eclipse.jetty.websocket.javax.server.tests package which is exported and to move all the tests to that. A better fix is needed. Signed-off-by: Greg Wilkins <gregw@webtide.com> * Issue #3872 Javax Websocket Packaging improve comments tighten exposed classes more Signed-off-by: Greg Wilkins <gregw@webtide.com> * Issue #3872 - fixing tests Signed-off-by: Lachlan Roberts <lachlan@webtide.com> * Issue #3872 - move ContainerDefaultConfigurator to config package Signed-off-by: Lachlan Roberts <lachlan@webtide.com> * Issue #3873 - fix javax websocket test classloader issues move websocket endpoints for test webapps to com.acme.websocket package Signed-off-by: Lachlan Roberts <lachlan@webtide.com> --- .../jetty/embedded/WebSocketJsrServer.java | 2 +- .../jetty-osgi-boot/jettyhome/etc/jetty.xml | 2 +- .../config/etc/jetty-with-custom-class.xml | 2 +- .../src/test/config/etc/jetty.xml | 2 +- .../src/main/java/module-info.java | 8 +- .../ContainerDefaultConfigurator.java | 2 +- .../JavaxWebSocketConfiguration.java | 5 +- ...xWebSocketServletContainerInitializer.java | 3 +- .../AnnotatedServerEndpointConfig.java | 2 +- .../JavaxWebSocketServerContainer.java | 6 +- ...vaxWebSocketServerFrameHandlerFactory.java | 4 +- .../UndefinedServerEndpointConfig.java | 2 +- .../javax.servlet.ServletContainerInitializer | 2 +- ...t.server.ServerEndpointConfig$Configurator | 2 +- .../org.eclipse.jetty.webapp.Configuration | 2 +- .../websocket/javax/server/PathParamTest.java | 61 ---------- .../server/browser/JsrBrowserDebugTool.java | 2 +- .../WebSocketServerExamplesTest.java | 20 ++-- .../websocket/javax/tests/LocalServer.java | 6 +- .../jetty/websocket/javax/tests/WSServer.java | 2 +- .../com/acme/websocket/BasicEchoEndpoint.java | 46 ++++++++ ...asicEchoEndpointConfigContextListener.java | 54 +++++++++ .../websocket/IdleTimeoutContextListener.java | 54 +++++++++ .../websocket/IdleTimeoutOnOpenEndpoint.java | 44 ++++++++ .../websocket/IdleTimeoutOnOpenSocket.java | 50 +++++++++ .../websocket/LargeEchoContextListener.java | 39 +++++++ .../websocket/LargeEchoDefaultSocket.java} | 14 ++- .../websocket/OnOpenIdleTimeoutEndpoint.java | 44 ++++++++ .../acme/websocket/PongContextListener.java | 61 ++++++++++ .../acme/websocket/PongMessageEndpoint.java | 66 +++++++++++ .../java/com/acme/websocket/PongSocket.java | 67 +++++++++++ .../websocket/javax/tests/PathParamTest.java | 104 ++++++++++++++++++ .../javax/tests/RestartContextTest.java | 4 +- ...tJavaxWebSocketServerFrameHandlerTest.java | 4 +- .../tests/server/DeploymentExceptionTest.java | 2 +- .../tests/server/EndpointViaConfigTest.java | 61 +--------- .../javax/tests/server/IdleTimeoutTest.java | 6 +- .../JettyServerEndpointConfiguratorTest.java | 2 +- .../tests/server/LargeContainerTest.java | 34 +----- .../javax/tests/server/MemoryUsageTest.java | 2 +- .../javax/tests/server/PingPongTest.java | 103 +---------------- .../WebSocketServerContainerExecutorTest.java | 6 +- .../basic-echo-endpoint-config-web.xml | 2 +- .../resources/idle-timeout-config-web.xml | 2 +- .../test/resources/large-echo-config-web.xml | 2 +- .../src/test/resources/pong-config-web.xml | 2 +- .../src/main/java/module-info.java | 1 + 47 files changed, 698 insertions(+), 315 deletions(-) rename jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/{ => config}/ContainerDefaultConfigurator.java (98%) rename jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/{ => config}/JavaxWebSocketConfiguration.java (89%) rename jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/{ => config}/JavaxWebSocketServletContainerInitializer.java (98%) rename jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/{ => internal}/JavaxWebSocketServerContainer.java (97%) rename jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/{ => internal}/JavaxWebSocketServerFrameHandlerFactory.java (92%) delete mode 100644 jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/PathParamTest.java rename jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/{ => examples}/WebSocketServerExamplesTest.java (90%) create mode 100644 jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/BasicEchoEndpoint.java create mode 100644 jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/BasicEchoEndpointConfigContextListener.java create mode 100644 jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/IdleTimeoutContextListener.java create mode 100644 jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/IdleTimeoutOnOpenEndpoint.java create mode 100644 jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/IdleTimeoutOnOpenSocket.java create mode 100644 jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/LargeEchoContextListener.java rename jetty-websocket/{javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/DummyServerContainer.java => javax-websocket-tests/src/test/java/com/acme/websocket/LargeEchoDefaultSocket.java} (70%) create mode 100644 jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/OnOpenIdleTimeoutEndpoint.java create mode 100644 jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/PongContextListener.java create mode 100644 jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/PongMessageEndpoint.java create mode 100644 jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/PongSocket.java create mode 100644 jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/PathParamTest.java diff --git a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/WebSocketJsrServer.java b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/WebSocketJsrServer.java index d3ac32dafbd..473fae972ab 100644 --- a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/WebSocketJsrServer.java +++ b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/WebSocketJsrServer.java @@ -26,7 +26,7 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.DefaultHandler; import org.eclipse.jetty.server.handler.HandlerList; import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServletContainerInitializer; +import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer; /** * Example of setting up a javax.websocket server with Jetty embedded diff --git a/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty.xml b/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty.xml index 17f38f2776d..70c174072ab 100644 --- a/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty.xml +++ b/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty.xml @@ -96,7 +96,7 @@ <Item>org.eclipse.jetty.webapp.JmxConfiguration</Item> <Item>org.eclipse.jetty.osgi.annotations.AnnotationConfiguration</Item> <Item>org.eclipse.jetty.websocket.server.config.JettyWebSocketConfiguration</Item> - <Item>org.eclipse.jetty.websocket.javax.server.JavaxWebSocketConfiguration</Item> + <Item>org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketConfiguration</Item> <Item>org.eclipse.jetty.osgi.boot.OSGiWebInfConfiguration</Item> <Item>org.eclipse.jetty.osgi.boot.OSGiMetaInfConfiguration</Item> </Array> diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-with-custom-class.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-with-custom-class.xml index 9cc5f6f72e7..6c2b9d68082 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-with-custom-class.xml +++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-with-custom-class.xml @@ -82,7 +82,7 @@ <Item>org.eclipse.jetty.plus.webapp.EnvConfiguration</Item> <Item>org.eclipse.jetty.webapp.JmxConfiguration</Item> <Item>org.eclipse.jetty.websocket.server.config.JettyWebSocketConfiguration</Item> - <Item>org.eclipse.jetty.websocket.javax.server.JavaxWebSocketConfiguration</Item> + <Item>org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketConfiguration</Item> <Item>org.eclipse.jetty.osgi.annotations.AnnotationConfiguration</Item> <Item>org.eclipse.jetty.osgi.boot.OSGiWebInfConfiguration</Item> <Item>org.eclipse.jetty.osgi.boot.OSGiMetaInfConfiguration</Item> diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty.xml index 291fccd1873..ad2b008c33a 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty.xml +++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty.xml @@ -85,7 +85,7 @@ <Item>org.eclipse.jetty.webapp.JmxConfiguration</Item> <Item>org.eclipse.jetty.osgi.annotations.AnnotationConfiguration</Item> <Item>org.eclipse.jetty.websocket.server.config.JettyWebSocketConfiguration</Item> - <Item>org.eclipse.jetty.websocket.javax.server.JavaxWebSocketConfiguration</Item> + <Item>org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketConfiguration</Item> <Item>org.eclipse.jetty.osgi.boot.OSGiWebInfConfiguration</Item> <Item>org.eclipse.jetty.osgi.boot.OSGiMetaInfConfiguration</Item> </Array> diff --git a/jetty-websocket/javax-websocket-server/src/main/java/module-info.java b/jetty-websocket/javax-websocket-server/src/main/java/module-info.java index 3c20f2e9d1d..8ef5adc7618 100644 --- a/jetty-websocket/javax-websocket-server/src/main/java/module-info.java +++ b/jetty-websocket/javax-websocket-server/src/main/java/module-info.java @@ -20,13 +20,13 @@ import javax.servlet.ServletContainerInitializer; import javax.websocket.server.ServerEndpointConfig; import org.eclipse.jetty.webapp.Configuration; -import org.eclipse.jetty.websocket.javax.server.ContainerDefaultConfigurator; -import org.eclipse.jetty.websocket.javax.server.JavaxWebSocketConfiguration; -import org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServletContainerInitializer; +import org.eclipse.jetty.websocket.javax.server.config.ContainerDefaultConfigurator; +import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketConfiguration; +import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer; module org.eclipse.jetty.websocket.javax.server { - exports org.eclipse.jetty.websocket.javax.server; + exports org.eclipse.jetty.websocket.javax.server.config; requires jetty.servlet.api; requires jetty.websocket.api; diff --git a/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/ContainerDefaultConfigurator.java b/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/config/ContainerDefaultConfigurator.java similarity index 98% rename from jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/ContainerDefaultConfigurator.java rename to jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/config/ContainerDefaultConfigurator.java index b4acdeb8b01..9906438cc3f 100644 --- a/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/ContainerDefaultConfigurator.java +++ b/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/config/ContainerDefaultConfigurator.java @@ -16,7 +16,7 @@ // ======================================================================== // -package org.eclipse.jetty.websocket.javax.server; +package org.eclipse.jetty.websocket.javax.server.config; import java.util.List; import java.util.ServiceLoader; diff --git a/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/JavaxWebSocketConfiguration.java b/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/config/JavaxWebSocketConfiguration.java similarity index 89% rename from jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/JavaxWebSocketConfiguration.java rename to jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/config/JavaxWebSocketConfiguration.java index 57255fad3b2..d0baa67fb5b 100644 --- a/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/JavaxWebSocketConfiguration.java +++ b/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/config/JavaxWebSocketConfiguration.java @@ -16,7 +16,7 @@ // ======================================================================== // -package org.eclipse.jetty.websocket.javax.server; +package org.eclipse.jetty.websocket.javax.server.config; import org.eclipse.jetty.webapp.AbstractConfiguration; import org.eclipse.jetty.webapp.FragmentConfiguration; @@ -38,6 +38,7 @@ public class JavaxWebSocketConfiguration extends AbstractConfiguration addDependencies(WebXmlConfiguration.class, MetaInfConfiguration.class, WebInfConfiguration.class, FragmentConfiguration.class); addDependents("org.eclipse.jetty.annotations.AnnotationConfiguration", WebAppConfiguration.class.getName()); protectAndExpose("org.eclipse.jetty.websocket.servlet."); // For WebSocketUpgradeFilter - protectAndExpose("org.eclipse.jetty.websocket.javax."); // TODO Do we need all classes? + protectAndExpose("org.eclipse.jetty.websocket.javax.server.config."); + hide("org.eclipse.jetty.websocket.javax.server.internal"); } } diff --git a/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/JavaxWebSocketServletContainerInitializer.java b/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/config/JavaxWebSocketServletContainerInitializer.java similarity index 98% rename from jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/JavaxWebSocketServletContainerInitializer.java rename to jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/config/JavaxWebSocketServletContainerInitializer.java index 9261f8356be..0780290b912 100644 --- a/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/JavaxWebSocketServletContainerInitializer.java +++ b/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/config/JavaxWebSocketServletContainerInitializer.java @@ -16,7 +16,7 @@ // ======================================================================== // -package org.eclipse.jetty.websocket.javax.server; +package org.eclipse.jetty.websocket.javax.server.config; import java.util.HashSet; import java.util.Set; @@ -39,6 +39,7 @@ import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.thread.ThreadClassLoaderScope; import org.eclipse.jetty.websocket.core.WebSocketComponents; +import org.eclipse.jetty.websocket.javax.server.internal.JavaxWebSocketServerContainer; import org.eclipse.jetty.websocket.servlet.WebSocketMapping; import org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter; diff --git a/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/AnnotatedServerEndpointConfig.java b/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/AnnotatedServerEndpointConfig.java index 277fa42daec..f3d03450cec 100644 --- a/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/AnnotatedServerEndpointConfig.java +++ b/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/AnnotatedServerEndpointConfig.java @@ -33,7 +33,7 @@ import javax.websocket.server.ServerEndpoint; import javax.websocket.server.ServerEndpointConfig; import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketContainer; -import org.eclipse.jetty.websocket.javax.server.ContainerDefaultConfigurator; +import org.eclipse.jetty.websocket.javax.server.config.ContainerDefaultConfigurator; public class AnnotatedServerEndpointConfig implements ServerEndpointConfig { diff --git a/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/JavaxWebSocketServerContainer.java b/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketServerContainer.java similarity index 97% rename from jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/JavaxWebSocketServerContainer.java rename to jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketServerContainer.java index ffe0ae7bc6b..276c1015cb7 100644 --- a/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/JavaxWebSocketServerContainer.java +++ b/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketServerContainer.java @@ -16,7 +16,7 @@ // ======================================================================== // -package org.eclipse.jetty.websocket.javax.server; +package org.eclipse.jetty.websocket.javax.server.internal; import java.util.ArrayList; import java.util.List; @@ -41,9 +41,7 @@ import org.eclipse.jetty.websocket.core.WebSocketComponents; import org.eclipse.jetty.websocket.core.WebSocketException; import org.eclipse.jetty.websocket.core.client.WebSocketCoreClient; import org.eclipse.jetty.websocket.javax.client.JavaxWebSocketClientContainer; -import org.eclipse.jetty.websocket.javax.server.internal.AnnotatedServerEndpointConfig; -import org.eclipse.jetty.websocket.javax.server.internal.JavaxWebSocketCreator; -import org.eclipse.jetty.websocket.javax.server.internal.UndefinedServerEndpointConfig; +import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer; import org.eclipse.jetty.websocket.servlet.WebSocketMapping; @ManagedObject("JSR356 Server Container") diff --git a/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/JavaxWebSocketServerFrameHandlerFactory.java b/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketServerFrameHandlerFactory.java similarity index 92% rename from jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/JavaxWebSocketServerFrameHandlerFactory.java rename to jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketServerFrameHandlerFactory.java index 8a5bdee8907..b16b5b2af02 100644 --- a/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/JavaxWebSocketServerFrameHandlerFactory.java +++ b/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketServerFrameHandlerFactory.java @@ -16,7 +16,7 @@ // ======================================================================== // -package org.eclipse.jetty.websocket.javax.server; +package org.eclipse.jetty.websocket.javax.server.internal; import javax.websocket.Endpoint; import javax.websocket.EndpointConfig; @@ -27,8 +27,6 @@ import org.eclipse.jetty.websocket.core.FrameHandler; import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketContainer; import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketFrameHandlerFactory; import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketFrameHandlerMetadata; -import org.eclipse.jetty.websocket.javax.server.internal.DelegatedJavaxServletUpgradeRequest; -import org.eclipse.jetty.websocket.javax.server.internal.PathParamIdentifier; import org.eclipse.jetty.websocket.servlet.FrameHandlerFactory; import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; diff --git a/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/UndefinedServerEndpointConfig.java b/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/UndefinedServerEndpointConfig.java index 8fdaf945db5..a5361a48d6a 100644 --- a/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/UndefinedServerEndpointConfig.java +++ b/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/UndefinedServerEndpointConfig.java @@ -27,7 +27,7 @@ import javax.websocket.Encoder; import javax.websocket.Extension; import javax.websocket.server.ServerEndpointConfig; -import org.eclipse.jetty.websocket.javax.server.ContainerDefaultConfigurator; +import org.eclipse.jetty.websocket.javax.server.config.ContainerDefaultConfigurator; public class UndefinedServerEndpointConfig implements ServerEndpointConfig { diff --git a/jetty-websocket/javax-websocket-server/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer b/jetty-websocket/javax-websocket-server/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer index 86c5fef3534..68d8b37077c 100644 --- a/jetty-websocket/javax-websocket-server/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer +++ b/jetty-websocket/javax-websocket-server/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer @@ -1 +1 @@ -org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServletContainerInitializer \ No newline at end of file +org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer \ No newline at end of file diff --git a/jetty-websocket/javax-websocket-server/src/main/resources/META-INF/services/javax.websocket.server.ServerEndpointConfig$Configurator b/jetty-websocket/javax-websocket-server/src/main/resources/META-INF/services/javax.websocket.server.ServerEndpointConfig$Configurator index b925d25755c..e9f7fc540a4 100644 --- a/jetty-websocket/javax-websocket-server/src/main/resources/META-INF/services/javax.websocket.server.ServerEndpointConfig$Configurator +++ b/jetty-websocket/javax-websocket-server/src/main/resources/META-INF/services/javax.websocket.server.ServerEndpointConfig$Configurator @@ -1 +1 @@ -org.eclipse.jetty.websocket.javax.server.ContainerDefaultConfigurator \ No newline at end of file +org.eclipse.jetty.websocket.javax.server.config.ContainerDefaultConfigurator \ No newline at end of file diff --git a/jetty-websocket/javax-websocket-server/src/main/resources/META-INF/services/org.eclipse.jetty.webapp.Configuration b/jetty-websocket/javax-websocket-server/src/main/resources/META-INF/services/org.eclipse.jetty.webapp.Configuration index 280be7ed694..9c5a60b0447 100644 --- a/jetty-websocket/javax-websocket-server/src/main/resources/META-INF/services/org.eclipse.jetty.webapp.Configuration +++ b/jetty-websocket/javax-websocket-server/src/main/resources/META-INF/services/org.eclipse.jetty.webapp.Configuration @@ -1 +1 @@ -org.eclipse.jetty.websocket.javax.server.JavaxWebSocketConfiguration \ No newline at end of file +org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketConfiguration \ No newline at end of file diff --git a/jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/PathParamTest.java b/jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/PathParamTest.java deleted file mode 100644 index 796ee693ca6..00000000000 --- a/jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/PathParamTest.java +++ /dev/null @@ -1,61 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. -// ------------------------------------------------------------------------ -// All rights reserved. This program and the accompanying materials -// are made available under the terms of the Eclipse Public License v1.0 -// and Apache License v2.0 which accompanies this distribution. -// -// The Eclipse Public License is available at -// http://www.eclipse.org/legal/epl-v10.html -// -// The Apache License v2.0 is available at -// http://www.opensource.org/licenses/apache2.0.php -// -// You may elect to redistribute this code under either of these licenses. -// ======================================================================== -// - -package org.eclipse.jetty.websocket.javax.server; - -import javax.websocket.DeploymentException; -import javax.websocket.OnMessage; -import javax.websocket.server.PathParam; -import javax.websocket.server.ServerEndpoint; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -public class PathParamTest -{ - private JavaxWebSocketServerContainer container; - - @BeforeEach - public void startContainer() throws Exception - { - container = new DummyServerContainer(); - container.start(); - } - - @AfterEach - public void stopContainer() throws Exception - { - container.stop(); - } - - @ServerEndpoint("/pathparam/basic/{name}") - public static class BasicPathParamSocket - { - @OnMessage - public void onMessage(String message, @PathParam("name") String name) - { - } - } - - @Test - public void testBasicPathParamSocket() throws DeploymentException - { - container.addEndpoint(BasicPathParamSocket.class); - } -} diff --git a/jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/browser/JsrBrowserDebugTool.java b/jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/browser/JsrBrowserDebugTool.java index ccb71039385..be0468ed9b3 100644 --- a/jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/browser/JsrBrowserDebugTool.java +++ b/jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/browser/JsrBrowserDebugTool.java @@ -33,7 +33,7 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.resource.Resource; -import org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServletContainerInitializer; +import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer; /** * Tool to help debug JSR based websocket circumstances reported around browsers. diff --git a/jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/WebSocketServerExamplesTest.java b/jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/examples/WebSocketServerExamplesTest.java similarity index 90% rename from jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/WebSocketServerExamplesTest.java rename to jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/examples/WebSocketServerExamplesTest.java index 56438ccfb47..dffb3e5179c 100644 --- a/jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/WebSocketServerExamplesTest.java +++ b/jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/examples/WebSocketServerExamplesTest.java @@ -16,7 +16,7 @@ // ======================================================================== // -package org.eclipse.jetty.websocket.javax.server; +package org.eclipse.jetty.websocket.javax.server.examples; import java.net.URI; import java.util.concurrent.ArrayBlockingQueue; @@ -45,9 +45,7 @@ import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.security.Constraint; import org.eclipse.jetty.util.security.Credential; -import org.eclipse.jetty.websocket.javax.server.examples.GetHttpSessionSocket; -import org.eclipse.jetty.websocket.javax.server.examples.MyAuthedSocket; -import org.eclipse.jetty.websocket.javax.server.examples.StreamingEchoSocket; +import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -93,15 +91,15 @@ public class WebSocketServerExamplesTest } static Server _server; - static ServerConnector connector; + static ServerConnector _connector; static ServletContextHandler _context; @BeforeAll public static void setup() throws Exception { _server = new Server(); - connector = new ServerConnector(_server); - _server.addConnector(connector); + _connector = new ServerConnector(_server); + _server.addConnector(_connector); _context = new ServletContextHandler(ServletContextHandler.SESSIONS); _context.setContextPath("/"); @@ -116,7 +114,7 @@ public class WebSocketServerExamplesTest }); _server.start(); - System.setProperty("org.eclipse.jetty.websocket.port", Integer.toString(connector.getLocalPort())); + System.setProperty("org.eclipse.jetty.websocket.port", Integer.toString(_connector.getLocalPort())); } @AfterAll @@ -155,7 +153,7 @@ public class WebSocketServerExamplesTest public void testMyAuthedSocket() throws Exception { //HttpClient is configured for BasicAuthentication with the XmlHttpClientProvider - URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/secured/socket"); + URI uri = URI.create("ws://localhost:" + _connector.getLocalPort() + "/secured/socket"); WebSocketContainer clientContainer = ContainerProvider.getWebSocketContainer(); ClientSocket clientEndpoint = new ClientSocket(); @@ -172,7 +170,7 @@ public class WebSocketServerExamplesTest @Test public void testStreamingEchoSocket() throws Exception { - URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/echo"); + URI uri = URI.create("ws://localhost:" + _connector.getLocalPort() + "/echo"); WebSocketContainer clientContainer = ContainerProvider.getWebSocketContainer(); ClientSocket clientEndpoint = new ClientSocket(); @@ -189,7 +187,7 @@ public class WebSocketServerExamplesTest @Test public void testGetHttpSessionSocket() throws Exception { - URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/example"); + URI uri = URI.create("ws://localhost:" + _connector.getLocalPort() + "/example"); WebSocketContainer clientContainer = ContainerProvider.getWebSocketContainer(); ClientSocket clientEndpoint = new ClientSocket(); diff --git a/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/LocalServer.java b/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/LocalServer.java index 5a638ab8cd0..9ed2a87aa3c 100644 --- a/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/LocalServer.java +++ b/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/LocalServer.java @@ -50,9 +50,9 @@ import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.websocket.core.internal.Parser; import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketSession; import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketSessionListener; -import org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServerContainer; -import org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServerFrameHandlerFactory; -import org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServletContainerInitializer; +import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer; +import org.eclipse.jetty.websocket.javax.server.internal.JavaxWebSocketServerContainer; +import org.eclipse.jetty.websocket.javax.server.internal.JavaxWebSocketServerFrameHandlerFactory; import org.eclipse.jetty.websocket.servlet.FrameHandlerFactory; import org.eclipse.jetty.websocket.servlet.WebSocketCreator; import org.eclipse.jetty.websocket.servlet.WebSocketServlet; diff --git a/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/WSServer.java b/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/WSServer.java index 817fcc2a7ed..52beae928fb 100644 --- a/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/WSServer.java +++ b/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/WSServer.java @@ -39,7 +39,7 @@ import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.resource.PathResource; import org.eclipse.jetty.webapp.WebAppContext; -import org.eclipse.jetty.websocket.javax.server.JavaxWebSocketConfiguration; +import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketConfiguration; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/BasicEchoEndpoint.java b/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/BasicEchoEndpoint.java new file mode 100644 index 00000000000..81ac929eb70 --- /dev/null +++ b/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/BasicEchoEndpoint.java @@ -0,0 +1,46 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package com.acme.websocket; + +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfig; +import javax.websocket.MessageHandler; +import javax.websocket.OnOpen; +import javax.websocket.Session; +import javax.websocket.server.ServerEndpoint; + +@ServerEndpoint("/echo") +public class BasicEchoEndpoint extends Endpoint implements MessageHandler.Whole<String> +{ + private Session session; + + @Override + public void onMessage(String msg) + { + // reply with echo + session.getAsyncRemote().sendText(msg); + } + + @OnOpen + public void onOpen(Session session, EndpointConfig config) + { + this.session = session; + session.addMessageHandler(this); + } +} diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/BasicEchoEndpointConfigContextListener.java b/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/BasicEchoEndpointConfigContextListener.java new file mode 100644 index 00000000000..4b8ddb988a8 --- /dev/null +++ b/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/BasicEchoEndpointConfigContextListener.java @@ -0,0 +1,54 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package com.acme.websocket; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.websocket.DeploymentException; +import javax.websocket.server.ServerEndpointConfig; + +public class BasicEchoEndpointConfigContextListener implements ServletContextListener +{ + @Override + public void contextDestroyed(ServletContextEvent sce) + { + /* do nothing */ + } + + @Override + public void contextInitialized(ServletContextEvent sce) + { + javax.websocket.server.ServerContainer container = (javax.websocket.server.ServerContainer)sce.getServletContext() + .getAttribute(javax.websocket.server.ServerContainer.class.getName()); + if (container == null) + throw new IllegalStateException("No Websocket ServerContainer in " + sce.getServletContext()); + + // Build up a configuration with a specific path + String path = "/echo"; + ServerEndpointConfig.Builder builder = ServerEndpointConfig.Builder.create(BasicEchoEndpoint.class, path); + try + { + container.addEndpoint(builder.build()); + } + catch (DeploymentException e) + { + throw new RuntimeException("Unable to add endpoint via config file", e); + } + } +} diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/IdleTimeoutContextListener.java b/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/IdleTimeoutContextListener.java new file mode 100644 index 00000000000..e58ba1b9726 --- /dev/null +++ b/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/IdleTimeoutContextListener.java @@ -0,0 +1,54 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package com.acme.websocket; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.websocket.DeploymentException; +import javax.websocket.server.ServerContainer; +import javax.websocket.server.ServerEndpointConfig; + +/** + * Example of adding a server WebSocket (extending {@link javax.websocket.Endpoint}) programmatically via config + */ +public class IdleTimeoutContextListener implements ServletContextListener +{ + @Override + public void contextDestroyed(ServletContextEvent sce) + { + /* do nothing */ + } + + @Override + public void contextInitialized(ServletContextEvent sce) + { + ServerContainer container = (ServerContainer)sce.getServletContext().getAttribute(ServerContainer.class.getName()); + // Build up a configuration with a specific path + String path = "/idle-onopen-endpoint"; + ServerEndpointConfig.Builder builder = ServerEndpointConfig.Builder.create(OnOpenIdleTimeoutEndpoint.class, path); + try + { + container.addEndpoint(builder.build()); + } + catch (DeploymentException e) + { + throw new RuntimeException("Unable to add endpoint via config file", e); + } + } +} diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/IdleTimeoutOnOpenEndpoint.java b/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/IdleTimeoutOnOpenEndpoint.java new file mode 100644 index 00000000000..8ad113ec775 --- /dev/null +++ b/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/IdleTimeoutOnOpenEndpoint.java @@ -0,0 +1,44 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package com.acme.websocket; + +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfig; +import javax.websocket.MessageHandler; +import javax.websocket.Session; + +public class IdleTimeoutOnOpenEndpoint extends Endpoint implements MessageHandler.Whole<String> +{ + private Session session; + + @Override + public void onOpen(Session session, EndpointConfig config) + { + this.session = session; + session.addMessageHandler(this); + session.setMaxIdleTimeout(500); + } + + @Override + public void onMessage(String message) + { + // echo message back (this is an indication of timeout failure) + session.getAsyncRemote().sendText(message); + } +} diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/IdleTimeoutOnOpenSocket.java b/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/IdleTimeoutOnOpenSocket.java new file mode 100644 index 00000000000..3278e6594db --- /dev/null +++ b/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/IdleTimeoutOnOpenSocket.java @@ -0,0 +1,50 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package com.acme.websocket; + +import javax.websocket.OnError; +import javax.websocket.OnMessage; +import javax.websocket.OnOpen; +import javax.websocket.Session; +import javax.websocket.server.ServerEndpoint; + +import org.eclipse.jetty.websocket.core.WebSocketTimeoutException; + +@ServerEndpoint(value = "/idle-onopen-socket") +public class IdleTimeoutOnOpenSocket +{ + @OnOpen + public void onOpen(Session session) + { + session.setMaxIdleTimeout(500); + } + + @OnMessage + public String onMessage(String msg) + { + return msg; + } + + @OnError + public void onError(Throwable cause) + { + if (!(cause instanceof WebSocketTimeoutException)) + throw new RuntimeException(cause); + } +} diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/LargeEchoContextListener.java b/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/LargeEchoContextListener.java new file mode 100644 index 00000000000..813caf986df --- /dev/null +++ b/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/LargeEchoContextListener.java @@ -0,0 +1,39 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package com.acme.websocket; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.websocket.server.ServerContainer; + +public class LargeEchoContextListener implements ServletContextListener +{ + @Override + public void contextDestroyed(ServletContextEvent sce) + { + /* do nothing */ + } + + @Override + public void contextInitialized(ServletContextEvent sce) + { + ServerContainer container = (ServerContainer)sce.getServletContext().getAttribute(ServerContainer.class.getName()); + container.setDefaultMaxTextMessageBufferSize(128 * 1024); + } +} diff --git a/jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/DummyServerContainer.java b/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/LargeEchoDefaultSocket.java similarity index 70% rename from jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/DummyServerContainer.java rename to jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/LargeEchoDefaultSocket.java index c39c5169bea..f5b9ddbfb9b 100644 --- a/jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/DummyServerContainer.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/LargeEchoDefaultSocket.java @@ -16,14 +16,18 @@ // ======================================================================== // -package org.eclipse.jetty.websocket.javax.server; +package com.acme.websocket; -import org.eclipse.jetty.websocket.servlet.WebSocketMapping; +import javax.websocket.OnMessage; +import javax.websocket.server.ServerEndpoint; -public class DummyServerContainer extends JavaxWebSocketServerContainer +@ServerEndpoint(value = "/echo/large") +public class LargeEchoDefaultSocket { - public DummyServerContainer() + @OnMessage + public void echo(javax.websocket.Session session, String msg) { - super(new WebSocketMapping()); + // reply with echo + session.getAsyncRemote().sendText(msg); } } diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/OnOpenIdleTimeoutEndpoint.java b/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/OnOpenIdleTimeoutEndpoint.java new file mode 100644 index 00000000000..71c305fa353 --- /dev/null +++ b/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/OnOpenIdleTimeoutEndpoint.java @@ -0,0 +1,44 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package com.acme.websocket; + +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfig; +import javax.websocket.MessageHandler; +import javax.websocket.Session; + +public class OnOpenIdleTimeoutEndpoint extends Endpoint implements MessageHandler.Whole<String> +{ + private Session session; + + @Override + public void onOpen(Session session, EndpointConfig config) + { + this.session = session; + session.addMessageHandler(this); + session.setMaxIdleTimeout(500); + } + + @Override + public void onMessage(String message) + { + // echo message back (this is an indication of timeout failure) + session.getAsyncRemote().sendText(message); + } +} diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/PongContextListener.java b/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/PongContextListener.java new file mode 100644 index 00000000000..68e47ecd086 --- /dev/null +++ b/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/PongContextListener.java @@ -0,0 +1,61 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package com.acme.websocket; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.websocket.DeploymentException; +import javax.websocket.HandshakeResponse; +import javax.websocket.server.HandshakeRequest; +import javax.websocket.server.ServerContainer; +import javax.websocket.server.ServerEndpointConfig; + +public class PongContextListener implements ServletContextListener +{ + public static class Config extends ServerEndpointConfig.Configurator + { + @Override + public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) + { + sec.getUserProperties().put("path", sec.getPath()); + super.modifyHandshake(sec, request, response); + } + } + + @Override + public void contextDestroyed(ServletContextEvent sce) + { + /* do nothing */ + } + + @Override + public void contextInitialized(ServletContextEvent sce) + { + ServerContainer container = (ServerContainer)sce.getServletContext().getAttribute(ServerContainer.class.getName()); + try + { + ServerEndpointConfig.Configurator config = new Config(); + container.addEndpoint(ServerEndpointConfig.Builder.create(PongMessageEndpoint.class, "/pong").configurator(config).build()); + } + catch (DeploymentException e) + { + throw new RuntimeException("Unable to add endpoint directly", e); + } + } +} diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/PongMessageEndpoint.java b/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/PongMessageEndpoint.java new file mode 100644 index 00000000000..b7253c13fdf --- /dev/null +++ b/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/PongMessageEndpoint.java @@ -0,0 +1,66 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package com.acme.websocket; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfig; +import javax.websocket.MessageHandler; +import javax.websocket.PongMessage; +import javax.websocket.Session; + +public class PongMessageEndpoint extends Endpoint implements MessageHandler.Whole<PongMessage> +{ + private String path = "?"; + private Session session; + + @Override + public void onOpen(Session session, EndpointConfig config) + { + this.session = session; + this.session.addMessageHandler(this); + this.path = (String)config.getUserProperties().get("path"); + } + + @Override + public void onMessage(PongMessage pong) + { + byte[] buf = toArray(pong.getApplicationData()); + String message = new String(buf, StandardCharsets.UTF_8); + this.session.getAsyncRemote().sendText("PongMessageEndpoint.onMessage(PongMessage):[" + path + "]:" + message); + } + + public static byte[] toArray(ByteBuffer buffer) + { + if (buffer.hasArray()) + { + byte[] array = buffer.array(); + int from = buffer.arrayOffset() + buffer.position(); + return Arrays.copyOfRange(array, from, from + buffer.remaining()); + } + else + { + byte[] to = new byte[buffer.remaining()]; + buffer.slice().get(to); + return to; + } + } +} diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/PongSocket.java b/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/PongSocket.java new file mode 100644 index 00000000000..5a0385d5ddb --- /dev/null +++ b/jetty-websocket/javax-websocket-tests/src/test/java/com/acme/websocket/PongSocket.java @@ -0,0 +1,67 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package com.acme.websocket; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import javax.websocket.EndpointConfig; +import javax.websocket.OnMessage; +import javax.websocket.OnOpen; +import javax.websocket.PongMessage; +import javax.websocket.Session; +import javax.websocket.server.ServerEndpoint; + +@ServerEndpoint(value = "/pong-socket", configurator = PongContextListener.Config.class) +public class PongSocket +{ + private String path = "?"; + private Session session; + + @OnOpen + public void onOpen(Session session, EndpointConfig config) + { + this.session = session; + this.path = (String)config.getUserProperties().get("path"); + } + + @OnMessage + public void onPong(PongMessage pong) + { + byte[] buf = toArray(pong.getApplicationData()); + String message = new String(buf, StandardCharsets.UTF_8); + this.session.getAsyncRemote().sendText("PongSocket.onPong(PongMessage)[" + path + "]:" + message); + } + + public static byte[] toArray(ByteBuffer buffer) + { + if (buffer.hasArray()) + { + byte[] array = buffer.array(); + int from = buffer.arrayOffset() + buffer.position(); + return Arrays.copyOfRange(array, from, from + buffer.remaining()); + } + else + { + byte[] to = new byte[buffer.remaining()]; + buffer.slice().get(to); + return to; + } + } +} diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/PathParamTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/PathParamTest.java new file mode 100644 index 00000000000..1796eb4caeb --- /dev/null +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/PathParamTest.java @@ -0,0 +1,104 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.javax.tests; + +import java.net.URI; +import java.util.concurrent.TimeUnit; +import javax.websocket.ContainerProvider; +import javax.websocket.OnMessage; +import javax.websocket.OnOpen; +import javax.websocket.Session; +import javax.websocket.WebSocketContainer; +import javax.websocket.server.PathParam; +import javax.websocket.server.ServerEndpoint; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +public class PathParamTest +{ + private Server _server; + private ServerConnector _connector; + private ServletContextHandler _context; + + @BeforeEach + public void startContainer() throws Exception + { + _server = new Server(); + _connector = new ServerConnector(_server); + _server.addConnector(_connector); + + _context = new ServletContextHandler(ServletContextHandler.SESSIONS); + _context.setContextPath("/"); + _server.setHandler(_context); + + JavaxWebSocketServletContainerInitializer.configure(_context, (context, container) -> + container.addEndpoint(EchoParamSocket.class)); + + _server.start(); + } + + @AfterEach + public void stopContainer() throws Exception + { + _server.stop(); + } + + @ServerEndpoint("/pathparam/echo/{name}") + public static class EchoParamSocket + { + private Session session; + + @OnOpen + public void onOpen(Session session) + { + this.session = session; + } + + @OnMessage + public void onMessage(String message, @PathParam("name") String name) + { + session.getAsyncRemote().sendText(message+"-"+name); + } + } + + @Test + public void testBasicPathParamSocket() throws Exception + { + WebSocketContainer container = ContainerProvider.getWebSocketContainer(); + EventSocket clientEndpoint = new EventSocket(); + + URI serverUri = URI.create("ws://localhost:"+ _connector.getLocalPort()+"/pathparam/echo/myParam"); + Session session = container.connectToServer(clientEndpoint, serverUri); + session.getBasicRemote().sendText("echo"); + + String resp = clientEndpoint.messageQueue.poll(1, TimeUnit.SECONDS); + assertThat("Response echo", resp, is("echo-myParam")); + session.close(); + clientEndpoint.closeLatch.await(5, TimeUnit.SECONDS); + } +} diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/RestartContextTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/RestartContextTest.java index 2499f5a5017..80a0265c382 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/RestartContextTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/RestartContextTest.java @@ -37,8 +37,8 @@ import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.DefaultHandler; import org.eclipse.jetty.server.handler.HandlerList; import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServerContainer; -import org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServletContainerInitializer; +import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer; +import org.eclipse.jetty.websocket.javax.server.internal.JavaxWebSocketServerContainer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/AbstractJavaxWebSocketServerFrameHandlerTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/AbstractJavaxWebSocketServerFrameHandlerTest.java index 548526309ee..ae920dae9ad 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/AbstractJavaxWebSocketServerFrameHandlerTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/AbstractJavaxWebSocketServerFrameHandlerTest.java @@ -27,8 +27,8 @@ import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.websocket.javax.client.EmptyClientEndpointConfig; import org.eclipse.jetty.websocket.javax.common.decoders.AvailableDecoders; import org.eclipse.jetty.websocket.javax.common.encoders.AvailableEncoders; -import org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServerContainer; -import org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServletContainerInitializer; +import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer; +import org.eclipse.jetty.websocket.javax.server.internal.JavaxWebSocketServerContainer; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/DeploymentExceptionTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/DeploymentExceptionTest.java index 052d8f87e3f..b38868cbfc0 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/DeploymentExceptionTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/DeploymentExceptionTest.java @@ -30,7 +30,7 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.HandlerCollection; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.websocket.javax.common.util.InvalidSignatureException; -import org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServletContainerInitializer; +import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer; import org.eclipse.jetty.websocket.javax.tests.server.sockets.InvalidCloseIntSocket; import org.eclipse.jetty.websocket.javax.tests.server.sockets.InvalidErrorErrorSocket; import org.eclipse.jetty.websocket.javax.tests.server.sockets.InvalidErrorIntSocket; diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/EndpointViaConfigTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/EndpointViaConfigTest.java index 69f27b6c05d..43f0e5dbe8c 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/EndpointViaConfigTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/EndpointViaConfigTest.java @@ -21,16 +21,9 @@ package org.eclipse.jetty.websocket.javax.tests.server; import java.net.URI; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; -import javax.websocket.DeploymentException; -import javax.websocket.EndpointConfig; -import javax.websocket.MessageHandler; -import javax.websocket.OnOpen; -import javax.websocket.Session; -import javax.websocket.server.ServerEndpoint; -import javax.websocket.server.ServerEndpointConfig; +import com.acme.websocket.BasicEchoEndpoint; +import com.acme.websocket.BasicEchoEndpointConfigContextListener; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.eclipse.jetty.util.Callback; @@ -41,7 +34,6 @@ import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.FrameHandler; import org.eclipse.jetty.websocket.core.OpCode; import org.eclipse.jetty.websocket.core.client.WebSocketCoreClient; -import org.eclipse.jetty.websocket.javax.tests.WSEventTracker; import org.eclipse.jetty.websocket.javax.tests.WSServer; import org.eclipse.jetty.websocket.javax.tests.framehandlers.FrameHandlerTracker; import org.junit.jupiter.api.Test; @@ -59,55 +51,6 @@ public class EndpointViaConfigTest { private static final Logger LOG = Log.getLogger(EndpointViaConfigTest.class); - @ServerEndpoint("/echo") - public static class BasicEchoEndpoint extends WSEventTracker implements MessageHandler.Whole<String> - { - @Override - public void onMessage(String msg) - { - super.onWsText(msg); - // reply with echo - session.getAsyncRemote().sendText(msg); - } - - @OnOpen - public void onOpen(Session session, EndpointConfig config) - { - super.onWsOpen(session, config); - this.session.addMessageHandler(this); - } - } - - public static class BasicEchoEndpointConfigContextListener implements ServletContextListener - { - @Override - public void contextDestroyed(ServletContextEvent sce) - { - /* do nothing */ - } - - @Override - public void contextInitialized(ServletContextEvent sce) - { - javax.websocket.server.ServerContainer container = (javax.websocket.server.ServerContainer)sce.getServletContext() - .getAttribute(javax.websocket.server.ServerContainer.class.getName()); - if (container == null) - throw new IllegalStateException("No Websocket ServerContainer in " + sce.getServletContext()); - - // Build up a configuration with a specific path - String path = "/echo"; - ServerEndpointConfig.Builder builder = ServerEndpointConfig.Builder.create(BasicEchoEndpoint.class, path); - try - { - container.addEndpoint(builder.build()); - } - catch (DeploymentException e) - { - throw new RuntimeException("Unable to add endpoint via config file", e); - } - } - } - public WorkDir testdir; @Test diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/IdleTimeoutTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/IdleTimeoutTest.java index 261f265c3d0..b3561a78b92 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/IdleTimeoutTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/IdleTimeoutTest.java @@ -21,6 +21,9 @@ package org.eclipse.jetty.websocket.javax.tests.server; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; +import com.acme.websocket.IdleTimeoutContextListener; +import com.acme.websocket.IdleTimeoutOnOpenEndpoint; +import com.acme.websocket.IdleTimeoutOnOpenSocket; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.log.StacklessLogging; import org.eclipse.jetty.webapp.WebAppContext; @@ -29,8 +32,6 @@ import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.OpCode; import org.eclipse.jetty.websocket.javax.tests.Fuzzer; import org.eclipse.jetty.websocket.javax.tests.WSServer; -import org.eclipse.jetty.websocket.javax.tests.server.sockets.IdleTimeoutOnOpenEndpoint; -import org.eclipse.jetty.websocket.javax.tests.server.sockets.IdleTimeoutOnOpenSocket; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -59,7 +60,6 @@ public class IdleTimeoutTest WebAppContext webapp = server.createWebAppContext(); server.deployWebapp(webapp); - // wsb.dump(); } @AfterAll diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/JettyServerEndpointConfiguratorTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/JettyServerEndpointConfiguratorTest.java index 0c28d087267..61ab2aca1f2 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/JettyServerEndpointConfiguratorTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/JettyServerEndpointConfiguratorTest.java @@ -22,7 +22,7 @@ import java.util.Iterator; import java.util.ServiceLoader; import javax.websocket.server.ServerEndpointConfig; -import org.eclipse.jetty.websocket.javax.server.ContainerDefaultConfigurator; +import org.eclipse.jetty.websocket.javax.server.config.ContainerDefaultConfigurator; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/LargeContainerTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/LargeContainerTest.java index 21ffd70a40a..fbdf26ce9db 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/LargeContainerTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/LargeContainerTest.java @@ -23,12 +23,8 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; -import javax.websocket.OnMessage; -import javax.websocket.server.ServerContainer; -import javax.websocket.server.ServerEndpoint; +import com.acme.websocket.LargeEchoDefaultSocket; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.eclipse.jetty.util.Callback; @@ -51,33 +47,6 @@ import static org.hamcrest.Matchers.is; @ExtendWith(WorkDirExtension.class) public class LargeContainerTest { - @ServerEndpoint(value = "/echo/large") - public static class LargeEchoDefaultSocket - { - @OnMessage - public void echo(javax.websocket.Session session, String msg) - { - // reply with echo - session.getAsyncRemote().sendText(msg); - } - } - - public static class LargeEchoContextListener implements ServletContextListener - { - @Override - public void contextDestroyed(ServletContextEvent sce) - { - /* do nothing */ - } - - @Override - public void contextInitialized(ServletContextEvent sce) - { - ServerContainer container = (ServerContainer)sce.getServletContext().getAttribute(ServerContainer.class.getName()); - container.setDefaultMaxTextMessageBufferSize(128 * 1024); - } - } - public WorkDir testdir; @SuppressWarnings("Duplicates") @@ -95,7 +64,6 @@ public class LargeContainerTest WebAppContext webapp = wsb.createWebAppContext(); wsb.deployWebapp(webapp); - // wsb.dump(); WebSocketCoreClient client = new WebSocketCoreClient(); try diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/MemoryUsageTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/MemoryUsageTest.java index e5e0a6821a6..1cb415d913f 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/MemoryUsageTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/MemoryUsageTest.java @@ -35,7 +35,7 @@ import javax.websocket.server.ServerEndpointConfig; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServletContainerInitializer; +import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/PingPongTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/PingPongTest.java index 1b44cdc23f0..e323ff87ea5 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/PingPongTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/PingPongTest.java @@ -19,33 +19,17 @@ package org.eclipse.jetty.websocket.javax.tests.server; import java.net.URI; -import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.time.Duration; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; -import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; -import javax.websocket.DeploymentException; -import javax.websocket.Endpoint; -import javax.websocket.EndpointConfig; -import javax.websocket.HandshakeResponse; -import javax.websocket.MessageHandler; -import javax.websocket.OnMessage; -import javax.websocket.OnOpen; -import javax.websocket.PongMessage; -import javax.websocket.Session; -import javax.websocket.server.HandshakeRequest; -import javax.websocket.server.ServerContainer; -import javax.websocket.server.ServerEndpoint; -import javax.websocket.server.ServerEndpointConfig; +import com.acme.websocket.PongContextListener; +import com.acme.websocket.PongMessageEndpoint; +import com.acme.websocket.PongSocket; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; -import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.util.log.Log; -import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.webapp.WebAppContext; import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.FrameHandler; @@ -64,87 +48,6 @@ import static org.junit.jupiter.api.Assertions.assertTimeout; public class PingPongTest { - @ServerEndpoint(value = "/pong-socket", configurator = PongContextListener.Config.class) - public static class PongSocket - { - private static final Logger LOG = Log.getLogger(PongSocket.class); - private String path = "?"; - private Session session; - - @OnOpen - public void onOpen(Session session, EndpointConfig config) - { - this.session = session; - this.path = (String)config.getUserProperties().get("path"); - } - - @OnMessage - public void onPong(PongMessage pong) - { - if (LOG.isDebugEnabled()) - LOG.debug("PongSocket.onPong(): PongMessage.appData={}", BufferUtil.toDetailString(pong.getApplicationData())); - byte[] buf = BufferUtil.toArray(pong.getApplicationData()); - String message = new String(buf, StandardCharsets.UTF_8); - this.session.getAsyncRemote().sendText("PongSocket.onPong(PongMessage)[" + path + "]:" + message); - } - } - - public static class PongMessageEndpoint extends Endpoint implements MessageHandler.Whole<PongMessage> - { - private String path = "?"; - private Session session; - - @Override - public void onOpen(Session session, EndpointConfig config) - { - this.session = session; - this.session.addMessageHandler(this); - this.path = (String)config.getUserProperties().get("path"); - } - - @Override - public void onMessage(PongMessage pong) - { - byte[] buf = BufferUtil.toArray(pong.getApplicationData()); - String message = new String(buf, StandardCharsets.UTF_8); - this.session.getAsyncRemote().sendText("PongMessageEndpoint.onMessage(PongMessage):[" + path + "]:" + message); - } - } - - public static class PongContextListener implements ServletContextListener - { - public static class Config extends ServerEndpointConfig.Configurator - { - @Override - public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) - { - sec.getUserProperties().put("path", sec.getPath()); - super.modifyHandshake(sec, request, response); - } - } - - @Override - public void contextDestroyed(ServletContextEvent sce) - { - /* do nothing */ - } - - @Override - public void contextInitialized(ServletContextEvent sce) - { - ServerContainer container = (ServerContainer)sce.getServletContext().getAttribute(ServerContainer.class.getName()); - try - { - ServerEndpointConfig.Configurator config = new Config(); - container.addEndpoint(ServerEndpointConfig.Builder.create(PongMessageEndpoint.class, "/pong").configurator(config).build()); - } - catch (DeploymentException e) - { - throw new RuntimeException("Unable to add endpoint directly", e); - } - } - } - private static WSServer server; private static WebSocketCoreClient client; diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/WebSocketServerContainerExecutorTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/WebSocketServerContainerExecutorTest.java index 7db6aa77bbe..e406aed6db6 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/WebSocketServerContainerExecutorTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/WebSocketServerContainerExecutorTest.java @@ -44,12 +44,12 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.thread.QueuedThreadPool; -import org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServerContainer; -import org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServletContainerInitializer; +import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer; +import org.eclipse.jetty.websocket.javax.server.internal.JavaxWebSocketServerContainer; import org.eclipse.jetty.websocket.javax.tests.WSURI; import org.junit.jupiter.api.Test; -import static org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServletContainerInitializer.HTTPCLIENT_ATTRIBUTE; +import static org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer.HTTPCLIENT_ATTRIBUTE; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.sameInstance; diff --git a/jetty-websocket/javax-websocket-tests/src/test/resources/basic-echo-endpoint-config-web.xml b/jetty-websocket/javax-websocket-tests/src/test/resources/basic-echo-endpoint-config-web.xml index 2daa96edb6c..191ba7fff8b 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/resources/basic-echo-endpoint-config-web.xml +++ b/jetty-websocket/javax-websocket-tests/src/test/resources/basic-echo-endpoint-config-web.xml @@ -7,6 +7,6 @@ version="3.0"> <listener> - <listener-class>org.eclipse.jetty.websocket.javax.tests.server.EndpointViaConfigTest$BasicEchoEndpointConfigContextListener</listener-class> + <listener-class>com.acme.websocket.BasicEchoEndpointConfigContextListener</listener-class> </listener> </web-app> \ No newline at end of file diff --git a/jetty-websocket/javax-websocket-tests/src/test/resources/idle-timeout-config-web.xml b/jetty-websocket/javax-websocket-tests/src/test/resources/idle-timeout-config-web.xml index ecb89d2acd2..a60cea6c700 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/resources/idle-timeout-config-web.xml +++ b/jetty-websocket/javax-websocket-tests/src/test/resources/idle-timeout-config-web.xml @@ -7,6 +7,6 @@ version="3.0"> <listener> - <listener-class>org.eclipse.jetty.websocket.javax.tests.server.IdleTimeoutContextListener</listener-class> + <listener-class>com.acme.websocket.IdleTimeoutContextListener</listener-class> </listener> </web-app> \ No newline at end of file diff --git a/jetty-websocket/javax-websocket-tests/src/test/resources/large-echo-config-web.xml b/jetty-websocket/javax-websocket-tests/src/test/resources/large-echo-config-web.xml index c4dfacddef3..b032926b059 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/resources/large-echo-config-web.xml +++ b/jetty-websocket/javax-websocket-tests/src/test/resources/large-echo-config-web.xml @@ -7,6 +7,6 @@ version="3.0"> <listener> - <listener-class>org.eclipse.jetty.websocket.javax.tests.server.LargeContainerTest$LargeEchoContextListener</listener-class> + <listener-class>com.acme.websocket.LargeEchoContextListener</listener-class> </listener> </web-app> \ No newline at end of file diff --git a/jetty-websocket/javax-websocket-tests/src/test/resources/pong-config-web.xml b/jetty-websocket/javax-websocket-tests/src/test/resources/pong-config-web.xml index aa16c7af156..a3478e35e2a 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/resources/pong-config-web.xml +++ b/jetty-websocket/javax-websocket-tests/src/test/resources/pong-config-web.xml @@ -7,6 +7,6 @@ version="3.0"> <listener> - <listener-class>org.eclipse.jetty.websocket.javax.tests.server.PingPongTest$PongContextListener</listener-class> + <listener-class>com.acme.websocket.PongContextListener</listener-class> </listener> </web-app> \ No newline at end of file diff --git a/jetty-websocket/jetty-websocket-server/src/main/java/module-info.java b/jetty-websocket/jetty-websocket-server/src/main/java/module-info.java index 689f680ecea..005c0f64d7f 100644 --- a/jetty-websocket/jetty-websocket-server/src/main/java/module-info.java +++ b/jetty-websocket/jetty-websocket-server/src/main/java/module-info.java @@ -25,6 +25,7 @@ import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerI module org.eclipse.jetty.websocket.jetty.server { exports org.eclipse.jetty.websocket.server; + exports org.eclipse.jetty.websocket.server.config; requires jetty.servlet.api; requires org.eclipse.jetty.http; From a2fc9b113b7175d7f09493413e4b86f3e0e946ec Mon Sep 17 00:00:00 2001 From: Jan Bartel <janb@webtide.com> Date: Wed, 28 Aug 2019 11:04:31 +1000 Subject: [PATCH 43/55] Fix and enhance session invalidation tests. Signed-off-by: Jan Bartel <janb@webtide.com> --- .../eclipse/jetty/server/session/Session.java | 1 + .../server/session/SessionHandlerTest.java | 11 -- .../session/SessionInvalidationTest.java | 138 ++++++++++++++++++ 3 files changed, 139 insertions(+), 11 deletions(-) create mode 100644 tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/SessionInvalidationTest.java 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 867afb9bad8..a14d5f5ab53 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 @@ -707,6 +707,7 @@ public class Session implements SessionHandler.SessionIf { try (Lock lock = _lock.lock()) { + checkValidForRead(); return _sessionData.getAttribute(name); } } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/session/SessionHandlerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/session/SessionHandlerTest.java index e119dad6f33..728982e3012 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/session/SessionHandlerTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/session/SessionHandlerTest.java @@ -38,15 +38,4 @@ public class SessionHandlerTest assertThrows(IllegalArgumentException.class,() -> sessionHandler.setSessionTrackingModes(new HashSet<>(Arrays.asList(SessionTrackingMode.SSL, SessionTrackingMode.URL)))); } - - @Test - public void testInvalidSessiongetLastAccessedTime() - { - Session session = new Session(new SessionHandler(), - new SessionData("sd" ,"", "", 0, 0, 0, 0)); - session.getLastAccessedTime(); - session.invalidate(); - assertThrows(IllegalStateException.class, () -> session.getLastAccessedTime()); - } - } diff --git a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/SessionInvalidationTest.java b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/SessionInvalidationTest.java new file mode 100644 index 00000000000..8984f7fe214 --- /dev/null +++ b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/SessionInvalidationTest.java @@ -0,0 +1,138 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.server.session; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * SessionInvalidationTest + * + * Test that various methods on sessions can't be accessed after invalidation + */ +public class SessionInvalidationTest +{ + @Test + public void testInvalidation() throws Exception + { + String contextPath = ""; + String servletMapping = "/server"; + int scavengePeriod = -1; + + DefaultSessionCacheFactory cacheFactory = new DefaultSessionCacheFactory(); + cacheFactory.setEvictionPolicy(SessionCache.NEVER_EVICT); + SessionDataStoreFactory storeFactory = new TestSessionDataStoreFactory(); + ((AbstractSessionDataStoreFactory)storeFactory).setGracePeriodSec(scavengePeriod); + + TestServer server = new TestServer(0, 0, scavengePeriod, + cacheFactory, storeFactory); + ServletContextHandler context = server.addContext(contextPath); + TestServlet servlet = new TestServlet(); + ServletHolder holder = new ServletHolder(servlet); + 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"); + assertTrue(sessionCookie != null); + + // 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()); + } + finally + { + client.stop(); + } + } + finally + { + server.stop(); + } + } + + public static class TestServlet extends HttpServlet + { + private static final long serialVersionUID = 1L; + + @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); + assertNotNull(session); + } + else if ("test".equals(action)) + { + HttpSession session = request.getSession(false); + assertNotNull(session); + + //invalidate existing session + session.invalidate(); + + assertThrows(IllegalStateException.class, () -> session.invalidate()); + assertThrows(IllegalStateException.class, () -> session.getLastAccessedTime()); + assertThrows(IllegalStateException.class, () -> session.getCreationTime()); + assertThrows(IllegalStateException.class, () -> session.getAttribute("foo")); + assertThrows(IllegalStateException.class, () -> session.getAttributeNames()); + assertThrows(IllegalStateException.class, () -> session.getValue("foo")); + assertThrows(IllegalStateException.class, () -> session.getValueNames()); + assertThrows(IllegalStateException.class, () -> session.putValue("a", "b")); + assertThrows(IllegalStateException.class, () -> session.removeAttribute("foo")); + assertThrows(IllegalStateException.class, () -> session.removeValue("foo")); + assertThrows(IllegalStateException.class, () -> session.setAttribute("a", "b")); + assertDoesNotThrow(() -> session.getId()); + + } + } + } +} From 37712d75a2c7d525ffccc42fb74a9f31398034a6 Mon Sep 17 00:00:00 2001 From: Jan Bartel <janb@webtide.com> Date: Wed, 28 Aug 2019 11:08:42 +1000 Subject: [PATCH 44/55] Issue #4027 Ensure AbstractSessionDataStore started or throws exception. (#4028) * Issue #4027 Ensure AbstractSessionDataStore started or throws exception. Signed-off-by: Jan Bartel <janb@webtide.com> --- .../jetty/server/session/AbstractSessionDataStore.java | 9 +++++++++ .../jetty/server/session/DefaultSessionIdManager.java | 7 ++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionDataStore.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionDataStore.java index 9accdb69b16..b876c2e81ef 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionDataStore.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionDataStore.java @@ -80,6 +80,9 @@ public abstract class AbstractSessionDataStore extends ContainerLifeCycle implem @Override public SessionData load(String id) throws Exception { + if (!isStarted()) + throw new IllegalStateException ("Not started"); + final AtomicReference<SessionData> reference = new AtomicReference<SessionData>(); final AtomicReference<Exception> exception = new AtomicReference<Exception>(); @@ -109,6 +112,9 @@ public abstract class AbstractSessionDataStore extends ContainerLifeCycle implem @Override public void store(String id, SessionData data) throws Exception { + if (!isStarted()) + throw new IllegalStateException("Not started"); + if (data == null) return; @@ -154,6 +160,9 @@ public abstract class AbstractSessionDataStore extends ContainerLifeCycle implem @Override public Set<String> getExpired(Set<String> candidates) { + if (!isStarted()) + throw new IllegalStateException ("Not started"); + try { return doGetExpired(candidates); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionIdManager.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionIdManager.java index 23ab41bd40a..fbe85430246 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionIdManager.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionIdManager.java @@ -471,9 +471,9 @@ public class DefaultSessionIdManager extends ContainerLifeCycle implements Sessi } /** - * Get SessionManager for every context. + * Get SessionHandler for every context. * - * @return all session managers + * @return all SessionHandlers that are running */ @Override public Set<SessionHandler> getSessionHandlers() @@ -484,7 +484,8 @@ public class DefaultSessionIdManager extends ContainerLifeCycle implements Sessi { for (Handler h : tmp) { - handlers.add((SessionHandler)h); + if (h.isStarted()) + handlers.add((SessionHandler)h); } } return handlers; From f4d95e0f2f451332b3c42a351e7b6aa67bcf1162 Mon Sep 17 00:00:00 2001 From: Jan Bartel <janb@webtide.com> Date: Wed, 28 Aug 2019 11:22:14 +1000 Subject: [PATCH 45/55] Remove blank lines for NullSessionCacheTest Signed-off-by: Jan Bartel <janb@webtide.com> --- .../jetty/server/session/NullSessionCacheTest.java | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/NullSessionCacheTest.java b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/NullSessionCacheTest.java index 3ae38cb6d0a..5e63b62101b 100644 --- a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/NullSessionCacheTest.java +++ b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/NullSessionCacheTest.java @@ -67,9 +67,7 @@ public class NullSessionCacheTest se.getSession().setAttribute("pv", new TestObject(count)); } } - - - + public static class TestObject implements HttpSessionActivationListener { int i; @@ -97,8 +95,7 @@ public class NullSessionCacheTest ++activates; } } - - + @Test public void testWritesWithPassivation() throws Exception { @@ -191,8 +188,6 @@ public class NullSessionCacheTest assertEquals(0, context.getSessionHandler()._sessionAttributeListeners.size()); } - - @Test public void testWriteThroughAlways() throws Exception { @@ -245,7 +240,7 @@ public class NullSessionCacheTest } @Test - public void testWriteThroughNew () throws Exception + public void testWriteThroughNew() throws Exception { Server server = new Server(); @@ -303,8 +298,7 @@ public class NullSessionCacheTest assertEquals(4, store._numSaves.get());//release session should write it out assertFalse(session.isResident()); } - - + @Test public void testNotCached() throws Exception { From 2979ed5046eaa6705df937c9b80cc5497adfe9c9 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt <joakim.erdfelt@gmail.com> Date: Tue, 27 Aug 2019 20:25:10 -0500 Subject: [PATCH 46/55] Fixes #4020 - Satisfy Container LifeCycle dumpable behaviors Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com> --- .../extensions/WebSocketExtensionFactory.java | 56 ++++++++++++++----- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/WebSocketExtensionFactory.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/WebSocketExtensionFactory.java index 706429567b8..f1ca84d635f 100644 --- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/WebSocketExtensionFactory.java +++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/WebSocketExtensionFactory.java @@ -18,6 +18,7 @@ package org.eclipse.jetty.websocket.common.extensions; +import java.io.IOException; import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -27,6 +28,7 @@ import java.util.zip.Deflater; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.component.ContainerLifeCycle; +import org.eclipse.jetty.util.component.Dumpable; import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.compression.CompressionPool; import org.eclipse.jetty.util.compression.DeflaterPool; @@ -38,9 +40,9 @@ import org.eclipse.jetty.websocket.api.extensions.ExtensionFactory; import org.eclipse.jetty.websocket.common.extensions.compress.CompressExtension; import org.eclipse.jetty.websocket.common.scopes.WebSocketContainerScope; -public class WebSocketExtensionFactory extends ExtensionFactory implements LifeCycle +public class WebSocketExtensionFactory extends ExtensionFactory implements LifeCycle, Dumpable { - private ContainerLifeCycle lifecycle; + private ContainerLifeCycle containerLifeCycle; private WebSocketContainerScope container; private ServiceLoader<Extension> extensionLoader = ServiceLoader.load(Extension.class); private Map<String, Class<? extends Extension>> availableExtensions; @@ -49,7 +51,7 @@ public class WebSocketExtensionFactory extends ExtensionFactory implements LifeC public WebSocketExtensionFactory(WebSocketContainerScope container) { - lifecycle = new ContainerLifeCycle(); + containerLifeCycle = new ContainerLifeCycle(); availableExtensions = new HashMap<>(); for (Extension ext : extensionLoader) { @@ -58,8 +60,8 @@ public class WebSocketExtensionFactory extends ExtensionFactory implements LifeC } this.container = container; - lifecycle.addBean(inflaterPool); - lifecycle.addBean(deflaterPool); + containerLifeCycle.addBean(inflaterPool); + containerLifeCycle.addBean(deflaterPool); } @Override @@ -153,60 +155,84 @@ public class WebSocketExtensionFactory extends ExtensionFactory implements LifeC @Override public void start() throws Exception { - lifecycle.start(); + containerLifeCycle.start(); } @Override public void stop() throws Exception { - lifecycle.stop(); + containerLifeCycle.stop(); } @Override public boolean isRunning() { - return lifecycle.isRunning(); + return containerLifeCycle.isRunning(); } @Override public boolean isStarted() { - return lifecycle.isStarted(); + return containerLifeCycle.isStarted(); } @Override public boolean isStarting() { - return lifecycle.isStarting(); + return containerLifeCycle.isStarting(); } @Override public boolean isStopping() { - return lifecycle.isStopping(); + return containerLifeCycle.isStopping(); } @Override public boolean isStopped() { - return lifecycle.isStopped(); + return containerLifeCycle.isStopped(); } @Override public boolean isFailed() { - return lifecycle.isFailed(); + return containerLifeCycle.isFailed(); } @Override public void addLifeCycleListener(Listener listener) { - lifecycle.addLifeCycleListener(listener); + containerLifeCycle.addLifeCycleListener(listener); } @Override public void removeLifeCycleListener(Listener listener) { - lifecycle.removeLifeCycleListener(listener); + containerLifeCycle.removeLifeCycleListener(listener); + } + + @Override + public String dump() + { + return containerLifeCycle.dump(); + } + + @Override + public String dumpSelf() + { + return containerLifeCycle.dumpSelf(); + } + + @Override + public void dump(Appendable out, String indent) throws IOException + { + containerLifeCycle.dump(out, indent); + } + + @Override + public String toString() + { + return String.format("%s@%x{%s}", WebSocketExtensionFactory.class.getName(), hashCode(), containerLifeCycle.getState()); } } From 4251a3e092868259b6439aa286f25b8b3d9f2e75 Mon Sep 17 00:00:00 2001 From: Jan Bartel <janb@webtide.com> Date: Wed, 28 Aug 2019 12:29:14 +1000 Subject: [PATCH 47/55] Issue #4022 Prevent Servlet adding another Servlet (#4024) * Issue #4022 Prevent Servlet adding Servlet and added unit tests. Signed-off-by: Jan Bartel <janb@webtide.com> --- .../jetty/servlet/ServletContextHandler.java | 17 +- .../eclipse/jetty/servlet/ServletHandler.java | 12 + .../servlet/ServletContextHandlerTest.java | 378 +++++++++++++++++- .../jetty/servlet/ServletHolderTest.java | 47 +++ 4 files changed, 444 insertions(+), 10 deletions(-) diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletContextHandler.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletContextHandler.java index ba98aee7515..db28cfdbdb7 100644 --- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletContextHandler.java +++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletContextHandler.java @@ -1067,10 +1067,13 @@ public class ServletContextHandler extends ContextHandler return new Dispatcher(context, name); } - private void checkDynamicName(String name) + private void checkDynamic(String name) { if (isStarted()) throw new IllegalStateException(); + + if (ServletContextHandler.this.getServletHandler().isInitialized()) + throw new IllegalStateException(); if (StringUtil.isBlank(name)) throw new IllegalStateException("Missing name"); @@ -1085,7 +1088,7 @@ public class ServletContextHandler extends ContextHandler @Override public FilterRegistration.Dynamic addFilter(String filterName, Class<? extends Filter> filterClass) { - checkDynamicName(filterName); + checkDynamic(filterName); final ServletHandler handler = ServletContextHandler.this.getServletHandler(); FilterHolder holder = handler.getFilter(filterName); @@ -1114,7 +1117,7 @@ public class ServletContextHandler extends ContextHandler @Override public FilterRegistration.Dynamic addFilter(String filterName, String className) { - checkDynamicName(filterName); + checkDynamic(filterName); final ServletHandler handler = ServletContextHandler.this.getServletHandler(); FilterHolder holder = handler.getFilter(filterName); @@ -1143,7 +1146,7 @@ public class ServletContextHandler extends ContextHandler @Override public FilterRegistration.Dynamic addFilter(String filterName, Filter filter) { - checkDynamicName(filterName); + checkDynamic(filterName); final ServletHandler handler = ServletContextHandler.this.getServletHandler(); FilterHolder holder = handler.getFilter(filterName); @@ -1173,7 +1176,7 @@ public class ServletContextHandler extends ContextHandler @Override public ServletRegistration.Dynamic addServlet(String servletName, Class<? extends Servlet> servletClass) { - checkDynamicName(servletName); + checkDynamic(servletName); final ServletHandler handler = ServletContextHandler.this.getServletHandler(); ServletHolder holder = handler.getServlet(servletName); @@ -1203,7 +1206,7 @@ public class ServletContextHandler extends ContextHandler @Override public ServletRegistration.Dynamic addServlet(String servletName, String className) { - checkDynamicName(servletName); + checkDynamic(servletName); final ServletHandler handler = ServletContextHandler.this.getServletHandler(); ServletHolder holder = handler.getServlet(servletName); @@ -1233,7 +1236,7 @@ public class ServletContextHandler extends ContextHandler @Override public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet) { - checkDynamicName(servletName); + checkDynamic(servletName); final ServletHandler handler = ServletContextHandler.this.getServletHandler(); ServletHolder holder = handler.getServlet(servletName); diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java index b730ed82b52..897a9ccba98 100644 --- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java +++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java @@ -116,6 +116,7 @@ public class ServletHandler extends ScopedHandler private PathMappings<ServletHolder> _servletPathMap; private ListenerHolder[] _listeners = new ListenerHolder[0]; + private boolean _initialized = false; @SuppressWarnings("unchecked") protected final ConcurrentMap<String, FilterChain>[] _chainCache = new ConcurrentMap[FilterMapping.ALL]; @@ -331,6 +332,7 @@ public class ServletHandler extends ScopedHandler _filterPathMappings = null; _filterNameMappings = null; _servletPathMap = null; + _initialized = false; } protected IdentityService getIdentityService() @@ -730,6 +732,8 @@ public class ServletHandler extends ScopedHandler public void initialize() throws Exception { + _initialized = true; + MultiException mx = new MultiException(); Stream.concat(Stream.concat( @@ -755,6 +759,14 @@ public class ServletHandler extends ScopedHandler mx.ifExceptionThrow(); } + + /** + * @return true if initialized has been called, false otherwise + */ + public boolean isInitialized() + { + return _initialized; + } /** * @return whether the filter chains are cached. diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ServletContextHandlerTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ServletContextHandlerTest.java index c870a679b94..14d7ccfa616 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ServletContextHandlerTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ServletContextHandlerTest.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; +import java.util.EnumSet; import java.util.EventListener; import java.util.List; import java.util.Objects; @@ -29,6 +30,11 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import javax.servlet.DispatcherType; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.FilterRegistration; import javax.servlet.Servlet; import javax.servlet.ServletContainerInitializer; import javax.servlet.ServletContext; @@ -37,10 +43,13 @@ import javax.servlet.ServletContextAttributeListener; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.ServletException; +import javax.servlet.ServletRegistration; +import javax.servlet.ServletRequest; import javax.servlet.ServletRequestAttributeEvent; import javax.servlet.ServletRequestAttributeListener; import javax.servlet.ServletRequestEvent; import javax.servlet.ServletRequestListener; +import javax.servlet.ServletResponse; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -71,6 +80,9 @@ import org.eclipse.jetty.server.session.SessionHandler; import org.eclipse.jetty.util.DecoratedObjectFactory; import org.eclipse.jetty.util.Decorator; import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.log.StacklessLogging; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -110,12 +122,13 @@ public class ServletContextHandlerTest public static class MySCIStarter extends AbstractLifeCycle implements ServletContextHandler.ServletContainerInitializerCaller { - MySCI _sci = new MySCI(); + ServletContainerInitializer _sci = null; ContextHandler.Context _ctx; - MySCIStarter(ContextHandler.Context ctx) + MySCIStarter(ContextHandler.Context ctx, ServletContainerInitializer sci) { _ctx = ctx; + _sci = sci; } @Override @@ -417,7 +430,7 @@ public class ServletContextHandlerTest _server.setHandler(contexts); ServletContextHandler root = new ServletContextHandler(contexts, "/"); - root.addBean(new MySCIStarter(root.getServletContext()), true); + root.addBean(new MySCIStarter(root.getServletContext(), new MySCI()), true); _server.start(); assertTrue((Boolean)root.getServletContext().getAttribute("MySCI.startup")); assertTrue((Boolean)root.getServletContext().getAttribute("MyContextListener.contextInitialized")); @@ -595,6 +608,180 @@ public class ServletContextHandlerTest assertEquals(0, __testServlets.get()); } + @Test + public void testAddServletFromServlet() throws Exception + { + //A servlet cannot be added by another servlet + Logger logger = Log.getLogger(ContextHandler.class.getName() + "ROOT"); + + try (StacklessLogging stackless = new StacklessLogging(logger)) + { + ServletContextHandler context = new ServletContextHandler(); + context.setLogger(logger); + ServletHolder holder = context.addServlet(ServletAddingServlet.class, "/start"); + context.getServletHandler().setStartWithUnavailable(false); + holder.setInitOrder(0); + context.setContextPath("/"); + _server.setHandler(context); + _server.start(); + fail("Servlet can only be added from SCI or SCL"); + } + catch (Exception e) + { + if (e instanceof ServletException) + { + assertTrue(e.getCause() instanceof IllegalStateException); + } + else + fail(e); + } + } + + @Test + public void testAddFilterFromServlet() throws Exception + { + //A filter cannot be added from a servlet + Logger logger = Log.getLogger(ContextHandler.class.getName() + "ROOT"); + + try (StacklessLogging stackless = new StacklessLogging(logger)) + { + ServletContextHandler context = new ServletContextHandler(); + context.setLogger(logger); + ServletHolder holder = context.addServlet(FilterAddingServlet.class, "/filter"); + context.getServletHandler().setStartWithUnavailable(false); + holder.setInitOrder(0); + context.setContextPath("/"); + _server.setHandler(context); + _server.start(); + fail("Filter can only be added from SCI or SCL"); + } + catch (Exception e) + { + if (e instanceof ServletException) + { + assertTrue(e.getCause() instanceof IllegalStateException); + } + else + fail(e); + } + } + + @Test + public void testAddServletFromFilter() throws Exception + { + //A servlet cannot be added from a Filter + Logger logger = Log.getLogger(ContextHandler.class.getName() + "ROOT"); + + try (StacklessLogging stackless = new StacklessLogging(logger)) + { + ServletContextHandler context = new ServletContextHandler(); + context.setLogger(logger); + FilterHolder holder = new FilterHolder(new Filter() + { + @Override + public void init(FilterConfig filterConfig) throws ServletException + { + ServletRegistration rego = filterConfig.getServletContext().addServlet("hello", HelloServlet.class); + rego.addMapping("/hello/*"); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException + { + } + + @Override + public void destroy() + { + } + + }); + context.addFilter(holder, "/*", EnumSet.of(DispatcherType.REQUEST)); + context.getServletHandler().setStartWithUnavailable(false); + context.setContextPath("/"); + _server.setHandler(context); + _server.start(); + fail("Servlet can only be added from SCI or SCL"); + } + catch (Exception e) + { + if (!(e instanceof IllegalStateException)) + { + if (e instanceof ServletException) + { + assertTrue(e.getCause() instanceof IllegalStateException); + } + else + fail(e); + } + } + } + + @Test + public void testAddServletFromSCL() throws Exception + { + //A servlet can be added from a ServletContextListener + ServletContextHandler context = new ServletContextHandler(); + context.getServletHandler().setStartWithUnavailable(false); + context.setContextPath("/"); + context.addEventListener(new ServletContextListener() + { + + @Override + public void contextInitialized(ServletContextEvent sce) + { + ServletRegistration rego = sce.getServletContext().addServlet("hello", HelloServlet.class); + rego.addMapping("/hello/*"); + } + + @Override + public void contextDestroyed(ServletContextEvent sce) + { + } + + }); + _server.setHandler(context); + _server.start(); + + StringBuffer request = new StringBuffer(); + request.append("GET /hello HTTP/1.0\n"); + request.append("Host: localhost\n"); + request.append("\n"); + + String response = _connector.getResponse(request.toString()); + assertThat("Response", response, containsString("Hello World")); + } + + @Test + public void testAddServletFromSCI() throws Exception + { + //A servlet can be added from a ServletContainerInitializer + ContextHandlerCollection contexts = new ContextHandlerCollection(); + _server.setHandler(contexts); + + ServletContextHandler root = new ServletContextHandler(contexts, "/"); + class ServletAddingSCI implements ServletContainerInitializer + { + @Override + public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException + { + ServletRegistration rego = ctx.addServlet("hello", HelloServlet.class); + rego.addMapping("/hello/*"); + } + } + root.addBean(new MySCIStarter(root.getServletContext(), new ServletAddingSCI()), true); + _server.start(); + + StringBuffer request = new StringBuffer(); + request.append("GET /hello HTTP/1.0\n"); + request.append("Host: localhost\n"); + request.append("\n"); + + String response = _connector.getResponse(request.toString()); + assertThat("Response", response, containsString("Hello World")); + } + @Test public void testAddServletAfterStart() throws Exception { @@ -623,7 +810,126 @@ public class ServletContextHandlerTest response = _connector.getResponse(request.toString()); assertThat("Response", response, containsString("Hello World")); } + + @Test + public void testServletRegistrationByClass() throws Exception + { + ServletContextHandler context = new ServletContextHandler(); + context.setContextPath("/"); + ServletRegistration reg = context.getServletContext().addServlet("test", TestServlet.class); + reg.addMapping("/test"); + _server.setHandler(context); + _server.start(); + + StringBuffer request = new StringBuffer(); + request.append("GET /test HTTP/1.0\n"); + request.append("Host: localhost\n"); + request.append("\n"); + + String response = _connector.getResponse(request.toString()); + assertThat("Response", response, containsString("Test")); + } + + @Test + public void testServletRegistrationByClassName() throws Exception + { + ServletContextHandler context = new ServletContextHandler(); + context.setContextPath("/"); + ServletRegistration reg = context.getServletContext().addServlet("test", TestServlet.class.getName()); + reg.addMapping("/test"); + + _server.setHandler(context); + _server.start(); + + StringBuffer request = new StringBuffer(); + request.append("GET /test HTTP/1.0\n"); + request.append("Host: localhost\n"); + request.append("\n"); + + String response = _connector.getResponse(request.toString()); + assertThat("Response", response, containsString("Test")); + } + + @Test + public void testPartialServletRegistrationByName() throws Exception + { + ServletContextHandler context = new ServletContextHandler(); + context.setContextPath("/"); + ServletHolder partial = new ServletHolder(); + partial.setName("test"); + context.addServlet(partial, "/test"); + + //complete partial servlet registration by providing name of the servlet class + ServletRegistration reg = context.getServletContext().addServlet("test", TestServlet.class.getName()); + assertNotNull(reg); + assertEquals(TestServlet.class.getName(), partial.getClassName()); + + _server.setHandler(context); + _server.start(); + + StringBuffer request = new StringBuffer(); + request.append("GET /test HTTP/1.0\n"); + request.append("Host: localhost\n"); + request.append("\n"); + + String response = _connector.getResponse(request.toString()); + assertThat("Response", response, containsString("Test")); + } + + @Test + public void testPartialServletRegistrationByClass() throws Exception + { + ServletContextHandler context = new ServletContextHandler(); + context.setContextPath("/"); + ServletHolder partial = new ServletHolder(); + partial.setName("test"); + context.addServlet(partial, "/test"); + + //complete partial servlet registration by providing the servlet class + ServletRegistration reg = context.getServletContext().addServlet("test", TestServlet.class); + assertNotNull(reg); + assertEquals(TestServlet.class.getName(), partial.getClassName()); + assertSame(TestServlet.class, partial.getHeldClass()); + + _server.setHandler(context); + _server.start(); + + StringBuffer request = new StringBuffer(); + request.append("GET /test HTTP/1.0\n"); + request.append("Host: localhost\n"); + request.append("\n"); + + String response = _connector.getResponse(request.toString()); + assertThat("Response", response, containsString("Test")); + } + + @Test + public void testNullServletRegistration() throws Exception + { + ServletContextHandler context = new ServletContextHandler(); + context.setContextPath("/"); + ServletHolder full = new ServletHolder(); + full.setName("test"); + full.setHeldClass(TestServlet.class); + context.addServlet(full, "/test"); + + //Must return null if the servlet has been fully defined previously + ServletRegistration reg = context.getServletContext().addServlet("test", TestServlet.class); + assertNull(reg); + + _server.setHandler(context); + _server.start(); + + StringBuffer request = new StringBuffer(); + request.append("GET /test HTTP/1.0\n"); + request.append("Host: localhost\n"); + request.append("\n"); + + String response = _connector.getResponse(request.toString()); + assertThat("Response", response, containsString("Test")); + } + @Test public void testHandlerBeforeServletHandler() throws Exception { @@ -948,6 +1254,28 @@ public class ServletContextHandlerTest writer.write("Hello World"); } } + + public static class MyFilter implements Filter + { + + @Override + public void init(FilterConfig filterConfig) throws ServletException + { + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, + ServletException + { + request.getServletContext().setAttribute("filter", "filter"); + chain.doFilter(request, response); + } + + @Override + public void destroy() + { + } + } public static class DummyUtilDecorator implements org.eclipse.jetty.util.Decorator { @@ -1009,6 +1337,50 @@ public class ServletContextHandlerTest } } + public static class ServletAddingServlet extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + resp.getWriter().write("Start"); + resp.getWriter().close(); + } + + @Override + public void init() throws ServletException + { + ServletRegistration dynamic = getServletContext().addServlet("added", AddedServlet.class); + dynamic.addMapping("/added/*"); + } + } + + public static class FilterAddingServlet extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + resp.getWriter().write("Filter"); + resp.getWriter().close(); + } + + @Override + public void init() throws ServletException + { + FilterRegistration dynamic = getServletContext().addFilter("filter", new MyFilter()); + dynamic.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*"); + } + } + + public static class AddedServlet extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + resp.getWriter().write("Added"); + resp.getWriter().close(); + } + } + public static class TestServlet extends HttpServlet { private static final long serialVersionUID = 1L; diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ServletHolderTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ServletHolderTest.java index 06a4d4c3933..f9553980962 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ServletHolderTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ServletHolderTest.java @@ -18,7 +18,11 @@ package org.eclipse.jetty.servlet; +import javax.servlet.ServletException; import javax.servlet.UnavailableException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.util.MultiException; @@ -31,8 +35,15 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import java.io.IOException; + public class ServletHolderTest { + + public static class FakeServlet extends HttpServlet + { + } + @Test public void testTransitiveCompareTo() throws Exception @@ -113,6 +124,42 @@ public class ServletHolderTest assertThat(e.getCause().getMessage(), containsString("foo")); } } + + @Test + public void testWithClass() throws Exception + { + //Test adding servlet by class + try (StacklessLogging stackless = new StacklessLogging(BaseHolder.class, ServletHandler.class, ContextHandler.class, ServletContextHandler.class)) + { + ServletContextHandler context = new ServletContextHandler(); + ServletHandler handler = context.getServletHandler(); + ServletHolder holder = new ServletHolder(); + holder.setName("foo"); + holder.setHeldClass(FakeServlet.class); + handler.addServlet(holder); + handler.start(); + assertTrue(holder.isAvailable()); + assertTrue(holder.isStarted()); + } + } + + @Test + public void testWithClassName() throws Exception + { + //Test adding servlet by classname + try (StacklessLogging stackless = new StacklessLogging(BaseHolder.class, ServletHandler.class, ContextHandler.class, ServletContextHandler.class)) + { + ServletContextHandler context = new ServletContextHandler(); + ServletHandler handler = context.getServletHandler(); + ServletHolder holder = new ServletHolder(); + holder.setName("foo"); + holder.setClassName("org.eclipse.jetty.servlet.ServletHolderTest$FakeServlet"); + handler.addServlet(holder); + handler.start(); + assertTrue(holder.isAvailable()); + assertTrue(holder.isStarted()); + } + } @Test public void testUnloadableClassName() throws Exception From b2ea6a0861efee1b337c9beef7d7db3ca0b86f12 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt <joakim.erdfelt@gmail.com> Date: Wed, 28 Aug 2019 10:43:15 -0500 Subject: [PATCH 48/55] Fixing Test Parameterization Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com> --- .../org/eclipse/jetty/util/URIUtilTest.java | 682 ++++++++++-------- 1 file changed, 392 insertions(+), 290 deletions(-) diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilTest.java index c2e7ad56c7d..0ea76fcef5f 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilTest.java @@ -26,6 +26,7 @@ import java.util.stream.Stream; 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; @@ -40,339 +41,442 @@ import static org.junit.jupiter.api.Assertions.assertTrue; @SuppressWarnings("SpellCheckingInspection") public class URIUtilTest { - @Test // TODO: Parameterize - public void testEncodePath() + public static Stream<Arguments> encodePathSource() + { + return Stream.of( + Arguments.of("/foo%23+;,:=/b a r/?info ", "/foo%2523+%3B,:=/b%20a%20r/%3Finfo%20"), + Arguments.of("/context/'list'/\"me\"/;<script>window.alert('xss');</script>", + "/context/%27list%27/%22me%22/%3B%3Cscript%3Ewindow.alert(%27xss%27)%3B%3C/script%3E"), + Arguments.of("test\u00f6?\u00f6:\u00df", "test%C3%B6%3F%C3%B6:%C3%9F"), + Arguments.of("test?\u00f6?\u00f6:\u00df", "test%3F%C3%B6%3F%C3%B6:%C3%9F") + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("encodePathSource") + public void testEncodePath(String rawPath, String expectedEncoded) { // test basic encode/decode StringBuilder buf = new StringBuilder(); - buf.setLength(0); - URIUtil.encodePath(buf, "/foo%23+;,:=/b a r/?info "); - assertEquals("/foo%2523+%3B,:=/b%20a%20r/%3Finfo%20", buf.toString()); - - assertEquals("/foo%2523+%3B,:=/b%20a%20r/%3Finfo%20", URIUtil.encodePath("/foo%23+;,:=/b a r/?info ")); + URIUtil.encodePath(buf, rawPath); + assertEquals(expectedEncoded, buf.toString()); + } + @Test + public void testEncodeString() + { + StringBuilder buf = new StringBuilder(); buf.setLength(0); URIUtil.encodeString(buf, "foo%23;,:=b a r", ";,= "); assertEquals("foo%2523%3b%2c:%3db%20a%20r", buf.toString()); - - buf.setLength(0); - URIUtil.encodePath(buf, "/context/'list'/\"me\"/;<script>window.alert('xss');</script>"); - assertEquals("/context/%27list%27/%22me%22/%3B%3Cscript%3Ewindow.alert(%27xss%27)%3B%3C/script%3E", buf.toString()); - - buf.setLength(0); - URIUtil.encodePath(buf, "test\u00f6?\u00f6:\u00df"); - assertEquals("test%C3%B6%3F%C3%B6:%C3%9F", buf.toString()); - - buf.setLength(0); - URIUtil.encodePath(buf, "test?\u00f6?\u00f6:\u00df"); - assertEquals("test%3F%C3%B6%3F%C3%B6:%C3%9F", buf.toString()); } - @Test // TODO: Parameterize - public void testDecodePath() + public static Stream<Arguments> decodePathSource() { - assertEquals(URIUtil.decodePath("xx/foo/barxx", 2, 8), "/foo/bar"); - assertEquals("/foo/bar", URIUtil.decodePath("/foo/bar")); - assertEquals("/f o/b r", URIUtil.decodePath("/f%20o/b%20r")); - assertEquals("/foo/bar", URIUtil.decodePath("/foo;ignore/bar;ignore")); - assertEquals("/fää/bar", URIUtil.decodePath("/f\u00e4\u00e4;ignore/bar;ignore")); - assertEquals("/f\u0629\u0629%23/bar", URIUtil.decodePath("/f%d8%a9%d8%a9%2523;ignore/bar;ignore")); + List<Arguments> arguments = new ArrayList<>(); + arguments.add(Arguments.of("/foo/bar", "/foo/bar")); - assertEquals("foo%23;,:=b a r", URIUtil.decodePath("foo%2523%3b%2c:%3db%20a%20r;rubbish")); - assertEquals("/foo/bar%23;,:=b a r=", URIUtil.decodePath("xxx/foo/bar%2523%3b%2c:%3db%20a%20r%3Dxxx;rubbish", 3, 35)); - assertEquals("f\u00e4\u00e4%23;,:=b a r=", URIUtil.decodePath("fää%2523%3b%2c:%3db%20a%20r%3D")); - assertEquals("f\u0629\u0629%23;,:=b a r", URIUtil.decodePath("f%d8%a9%d8%a9%2523%3b%2c:%3db%20a%20r")); + arguments.add(Arguments.of("/f%20o/b%20r", "/f o/b r")); + arguments.add(Arguments.of("fää%2523%3b%2c:%3db%20a%20r%3D", "f\u00e4\u00e4%23;,:=b a r=")); + arguments.add(Arguments.of("f%d8%a9%d8%a9%2523%3b%2c:%3db%20a%20r", "f\u0629\u0629%23;,:=b a r")); + + // path parameters should be ignored + arguments.add(Arguments.of("/foo;ignore/bar;ignore", "/foo/bar")); + arguments.add(Arguments.of("/f\u00e4\u00e4;ignore/bar;ignore", "/fää/bar")); + arguments.add(Arguments.of("/f%d8%a9%d8%a9%2523;ignore/bar;ignore", "/f\u0629\u0629%23/bar")); + arguments.add(Arguments.of("foo%2523%3b%2c:%3db%20a%20r;rubbish", "foo%23;,:=b a r")); // Test for null character (real world ugly test case) byte[] oddBytes = {'/', 0x00, '/'}; String odd = new String(oddBytes, StandardCharsets.ISO_8859_1); assertEquals(odd, URIUtil.decodePath("/%00/")); + arguments.add(Arguments.of("/%00/", odd)); + + return arguments.stream(); } - @Test // TODO: Parameterize - public void testAddEncodedPaths() + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("decodePathSource") + public void testDecodePath(String encodedPath, String expectedPath) { - assertEquals(URIUtil.addEncodedPaths(null, null), null, "null+null"); - assertEquals(URIUtil.addEncodedPaths(null, ""), "", "null+"); - assertEquals(URIUtil.addEncodedPaths(null, "bbb"), "bbb", "null+bbb"); - assertEquals(URIUtil.addEncodedPaths(null, "/"), "/", "null+/"); - assertEquals(URIUtil.addEncodedPaths(null, "/bbb"), "/bbb", "null+/bbb"); - - assertEquals(URIUtil.addEncodedPaths("", null), "", "+null"); - assertEquals(URIUtil.addEncodedPaths("", ""), "", "+"); - assertEquals(URIUtil.addEncodedPaths("", "bbb"), "bbb", "+bbb"); - assertEquals(URIUtil.addEncodedPaths("", "/"), "/", "+/"); - assertEquals(URIUtil.addEncodedPaths("", "/bbb"), "/bbb", "+/bbb"); - - assertEquals(URIUtil.addEncodedPaths("aaa", null), "aaa", "aaa+null"); - assertEquals(URIUtil.addEncodedPaths("aaa", ""), "aaa", "aaa+"); - assertEquals(URIUtil.addEncodedPaths("aaa", "bbb"), "aaa/bbb", "aaa+bbb"); - assertEquals(URIUtil.addEncodedPaths("aaa", "/"), "aaa/", "aaa+/"); - assertEquals(URIUtil.addEncodedPaths("aaa", "/bbb"), "aaa/bbb", "aaa+/bbb"); - - assertEquals(URIUtil.addEncodedPaths("/", null), "/", "/+null"); - assertEquals(URIUtil.addEncodedPaths("/", ""), "/", "/+"); - assertEquals(URIUtil.addEncodedPaths("/", "bbb"), "/bbb", "/+bbb"); - assertEquals(URIUtil.addEncodedPaths("/", "/"), "/", "/+/"); - assertEquals(URIUtil.addEncodedPaths("/", "/bbb"), "/bbb", "/+/bbb"); - - assertEquals(URIUtil.addEncodedPaths("aaa/", null), "aaa/", "aaa/+null"); - assertEquals(URIUtil.addEncodedPaths("aaa/", ""), "aaa/", "aaa/+"); - assertEquals(URIUtil.addEncodedPaths("aaa/", "bbb"), "aaa/bbb", "aaa/+bbb"); - assertEquals(URIUtil.addEncodedPaths("aaa/", "/"), "aaa/", "aaa/+/"); - assertEquals(URIUtil.addEncodedPaths("aaa/", "/bbb"), "aaa/bbb", "aaa/+/bbb"); - - assertEquals(URIUtil.addEncodedPaths(";JS", null), ";JS", ";JS+null"); - assertEquals(URIUtil.addEncodedPaths(";JS", ""), ";JS", ";JS+"); - assertEquals(URIUtil.addEncodedPaths(";JS", "bbb"), "bbb;JS", ";JS+bbb"); - assertEquals(URIUtil.addEncodedPaths(";JS", "/"), "/;JS", ";JS+/"); - assertEquals(URIUtil.addEncodedPaths(";JS", "/bbb"), "/bbb;JS", ";JS+/bbb"); - - assertEquals(URIUtil.addEncodedPaths("aaa;JS", null), "aaa;JS", "aaa;JS+null"); - assertEquals(URIUtil.addEncodedPaths("aaa;JS", ""), "aaa;JS", "aaa;JS+"); - assertEquals(URIUtil.addEncodedPaths("aaa;JS", "bbb"), "aaa/bbb;JS", "aaa;JS+bbb"); - assertEquals(URIUtil.addEncodedPaths("aaa;JS", "/"), "aaa/;JS", "aaa;JS+/"); - assertEquals(URIUtil.addEncodedPaths("aaa;JS", "/bbb"), "aaa/bbb;JS", "aaa;JS+/bbb"); - - assertEquals(URIUtil.addEncodedPaths("aaa/;JS", null), "aaa/;JS", "aaa;JS+null"); - assertEquals(URIUtil.addEncodedPaths("aaa/;JS", ""), "aaa/;JS", "aaa;JS+"); - assertEquals(URIUtil.addEncodedPaths("aaa/;JS", "bbb"), "aaa/bbb;JS", "aaa;JS+bbb"); - assertEquals(URIUtil.addEncodedPaths("aaa/;JS", "/"), "aaa/;JS", "aaa;JS+/"); - assertEquals(URIUtil.addEncodedPaths("aaa/;JS", "/bbb"), "aaa/bbb;JS", "aaa;JS+/bbb"); - - assertEquals(URIUtil.addEncodedPaths("?A=1", null), "?A=1", "?A=1+null"); - assertEquals(URIUtil.addEncodedPaths("?A=1", ""), "?A=1", "?A=1+"); - assertEquals(URIUtil.addEncodedPaths("?A=1", "bbb"), "bbb?A=1", "?A=1+bbb"); - assertEquals(URIUtil.addEncodedPaths("?A=1", "/"), "/?A=1", "?A=1+/"); - assertEquals(URIUtil.addEncodedPaths("?A=1", "/bbb"), "/bbb?A=1", "?A=1+/bbb"); - - assertEquals(URIUtil.addEncodedPaths("aaa?A=1", null), "aaa?A=1", "aaa?A=1+null"); - assertEquals(URIUtil.addEncodedPaths("aaa?A=1", ""), "aaa?A=1", "aaa?A=1+"); - assertEquals(URIUtil.addEncodedPaths("aaa?A=1", "bbb"), "aaa/bbb?A=1", "aaa?A=1+bbb"); - assertEquals(URIUtil.addEncodedPaths("aaa?A=1", "/"), "aaa/?A=1", "aaa?A=1+/"); - assertEquals(URIUtil.addEncodedPaths("aaa?A=1", "/bbb"), "aaa/bbb?A=1", "aaa?A=1+/bbb"); - - assertEquals(URIUtil.addEncodedPaths("aaa/?A=1", null), "aaa/?A=1", "aaa?A=1+null"); - assertEquals(URIUtil.addEncodedPaths("aaa/?A=1", ""), "aaa/?A=1", "aaa?A=1+"); - assertEquals(URIUtil.addEncodedPaths("aaa/?A=1", "bbb"), "aaa/bbb?A=1", "aaa?A=1+bbb"); - assertEquals(URIUtil.addEncodedPaths("aaa/?A=1", "/"), "aaa/?A=1", "aaa?A=1+/"); - assertEquals(URIUtil.addEncodedPaths("aaa/?A=1", "/bbb"), "aaa/bbb?A=1", "aaa?A=1+/bbb"); - - assertEquals(URIUtil.addEncodedPaths(";JS?A=1", null), ";JS?A=1", ";JS?A=1+null"); - assertEquals(URIUtil.addEncodedPaths(";JS?A=1", ""), ";JS?A=1", ";JS?A=1+"); - assertEquals(URIUtil.addEncodedPaths(";JS?A=1", "bbb"), "bbb;JS?A=1", ";JS?A=1+bbb"); - assertEquals(URIUtil.addEncodedPaths(";JS?A=1", "/"), "/;JS?A=1", ";JS?A=1+/"); - assertEquals(URIUtil.addEncodedPaths(";JS?A=1", "/bbb"), "/bbb;JS?A=1", ";JS?A=1+/bbb"); - - assertEquals(URIUtil.addEncodedPaths("aaa;JS?A=1", null), "aaa;JS?A=1", "aaa;JS?A=1+null"); - assertEquals(URIUtil.addEncodedPaths("aaa;JS?A=1", ""), "aaa;JS?A=1", "aaa;JS?A=1+"); - assertEquals(URIUtil.addEncodedPaths("aaa;JS?A=1", "bbb"), "aaa/bbb;JS?A=1", "aaa;JS?A=1+bbb"); - assertEquals(URIUtil.addEncodedPaths("aaa;JS?A=1", "/"), "aaa/;JS?A=1", "aaa;JS?A=1+/"); - assertEquals(URIUtil.addEncodedPaths("aaa;JS?A=1", "/bbb"), "aaa/bbb;JS?A=1", "aaa;JS?A=1+/bbb"); - - assertEquals(URIUtil.addEncodedPaths("aaa/;JS?A=1", null), "aaa/;JS?A=1", "aaa;JS?A=1+null"); - assertEquals(URIUtil.addEncodedPaths("aaa/;JS?A=1", ""), "aaa/;JS?A=1", "aaa;JS?A=1+"); - assertEquals(URIUtil.addEncodedPaths("aaa/;JS?A=1", "bbb"), "aaa/bbb;JS?A=1", "aaa;JS?A=1+bbb"); - assertEquals(URIUtil.addEncodedPaths("aaa/;JS?A=1", "/"), "aaa/;JS?A=1", "aaa;JS?A=1+/"); - assertEquals(URIUtil.addEncodedPaths("aaa/;JS?A=1", "/bbb"), "aaa/bbb;JS?A=1", "aaa;JS?A=1+/bbb"); + String path = URIUtil.decodePath(encodedPath); + assertEquals(expectedPath, path); } - @Test // TODO: Parameterize - public void testAddDecodedPaths() + @Test + public void testDecodePathSubstring() { - assertEquals(URIUtil.addPaths(null, null), null, "null+null"); - assertEquals(URIUtil.addPaths(null, ""), "", "null+"); - assertEquals(URIUtil.addPaths(null, "bbb"), "bbb", "null+bbb"); - assertEquals(URIUtil.addPaths(null, "/"), "/", "null+/"); - assertEquals(URIUtil.addPaths(null, "/bbb"), "/bbb", "null+/bbb"); + String path = URIUtil.decodePath("xx/foo/barxx", 2, 8); + assertEquals("/foo/bar", path); - assertEquals(URIUtil.addPaths("", null), "", "+null"); - assertEquals(URIUtil.addPaths("", ""), "", "+"); - assertEquals(URIUtil.addPaths("", "bbb"), "bbb", "+bbb"); - assertEquals(URIUtil.addPaths("", "/"), "/", "+/"); - assertEquals(URIUtil.addPaths("", "/bbb"), "/bbb", "+/bbb"); - - assertEquals(URIUtil.addPaths("aaa", null), "aaa", "aaa+null"); - assertEquals(URIUtil.addPaths("aaa", ""), "aaa", "aaa+"); - assertEquals(URIUtil.addPaths("aaa", "bbb"), "aaa/bbb", "aaa+bbb"); - assertEquals(URIUtil.addPaths("aaa", "/"), "aaa/", "aaa+/"); - assertEquals(URIUtil.addPaths("aaa", "/bbb"), "aaa/bbb", "aaa+/bbb"); - - assertEquals(URIUtil.addPaths("/", null), "/", "/+null"); - assertEquals(URIUtil.addPaths("/", ""), "/", "/+"); - assertEquals(URIUtil.addPaths("/", "bbb"), "/bbb", "/+bbb"); - assertEquals(URIUtil.addPaths("/", "/"), "/", "/+/"); - assertEquals(URIUtil.addPaths("/", "/bbb"), "/bbb", "/+/bbb"); - - assertEquals(URIUtil.addPaths("aaa/", null), "aaa/", "aaa/+null"); - assertEquals(URIUtil.addPaths("aaa/", ""), "aaa/", "aaa/+"); - assertEquals(URIUtil.addPaths("aaa/", "bbb"), "aaa/bbb", "aaa/+bbb"); - assertEquals(URIUtil.addPaths("aaa/", "/"), "aaa/", "aaa/+/"); - assertEquals(URIUtil.addPaths("aaa/", "/bbb"), "aaa/bbb", "aaa/+/bbb"); - - assertEquals(URIUtil.addPaths(";JS", null), ";JS", ";JS+null"); - assertEquals(URIUtil.addPaths(";JS", ""), ";JS", ";JS+"); - assertEquals(URIUtil.addPaths(";JS", "bbb"), ";JS/bbb", ";JS+bbb"); - assertEquals(URIUtil.addPaths(";JS", "/"), ";JS/", ";JS+/"); - assertEquals(URIUtil.addPaths(";JS", "/bbb"), ";JS/bbb", ";JS+/bbb"); - - assertEquals(URIUtil.addPaths("aaa;JS", null), "aaa;JS", "aaa;JS+null"); - assertEquals(URIUtil.addPaths("aaa;JS", ""), "aaa;JS", "aaa;JS+"); - assertEquals(URIUtil.addPaths("aaa;JS", "bbb"), "aaa;JS/bbb", "aaa;JS+bbb"); - assertEquals(URIUtil.addPaths("aaa;JS", "/"), "aaa;JS/", "aaa;JS+/"); - assertEquals(URIUtil.addPaths("aaa;JS", "/bbb"), "aaa;JS/bbb", "aaa;JS+/bbb"); - - assertEquals(URIUtil.addPaths("aaa/;JS", null), "aaa/;JS", "aaa;JS+null"); - assertEquals(URIUtil.addPaths("aaa/;JS", ""), "aaa/;JS", "aaa;JS+"); - assertEquals(URIUtil.addPaths("aaa/;JS", "bbb"), "aaa/;JS/bbb", "aaa;JS+bbb"); - assertEquals(URIUtil.addPaths("aaa/;JS", "/"), "aaa/;JS/", "aaa;JS+/"); - assertEquals(URIUtil.addPaths("aaa/;JS", "/bbb"), "aaa/;JS/bbb", "aaa;JS+/bbb"); - - assertEquals(URIUtil.addPaths("?A=1", null), "?A=1", "?A=1+null"); - assertEquals(URIUtil.addPaths("?A=1", ""), "?A=1", "?A=1+"); - assertEquals(URIUtil.addPaths("?A=1", "bbb"), "?A=1/bbb", "?A=1+bbb"); - assertEquals(URIUtil.addPaths("?A=1", "/"), "?A=1/", "?A=1+/"); - assertEquals(URIUtil.addPaths("?A=1", "/bbb"), "?A=1/bbb", "?A=1+/bbb"); - - assertEquals(URIUtil.addPaths("aaa?A=1", null), "aaa?A=1", "aaa?A=1+null"); - assertEquals(URIUtil.addPaths("aaa?A=1", ""), "aaa?A=1", "aaa?A=1+"); - assertEquals(URIUtil.addPaths("aaa?A=1", "bbb"), "aaa?A=1/bbb", "aaa?A=1+bbb"); - assertEquals(URIUtil.addPaths("aaa?A=1", "/"), "aaa?A=1/", "aaa?A=1+/"); - assertEquals(URIUtil.addPaths("aaa?A=1", "/bbb"), "aaa?A=1/bbb", "aaa?A=1+/bbb"); - - assertEquals(URIUtil.addPaths("aaa/?A=1", null), "aaa/?A=1", "aaa?A=1+null"); - assertEquals(URIUtil.addPaths("aaa/?A=1", ""), "aaa/?A=1", "aaa?A=1+"); - assertEquals(URIUtil.addPaths("aaa/?A=1", "bbb"), "aaa/?A=1/bbb", "aaa?A=1+bbb"); - assertEquals(URIUtil.addPaths("aaa/?A=1", "/"), "aaa/?A=1/", "aaa?A=1+/"); - assertEquals(URIUtil.addPaths("aaa/?A=1", "/bbb"), "aaa/?A=1/bbb", "aaa?A=1+/bbb"); + path = URIUtil.decodePath("xxx/foo/bar%2523%3b%2c:%3db%20a%20r%3Dxxx;rubbish", 3, 35); + assertEquals("/foo/bar%23;,:=b a r=", path); } - @Test // TODO: Parameterize - public void testCompactPath() + public static Stream<Arguments> addEncodedPathsSource() { - assertEquals("/foo/bar", URIUtil.compactPath("/foo/bar")); - assertEquals("/foo/bar?a=b//c", URIUtil.compactPath("/foo/bar?a=b//c")); + return Stream.of( + Arguments.of(null, null, null), + Arguments.of(null, "", ""), + Arguments.of(null, "bbb", "bbb"), + Arguments.of(null, "/", "/"), + Arguments.of(null, "/bbb", "/bbb"), - assertEquals("/foo/bar", URIUtil.compactPath("//foo//bar")); - assertEquals("/foo/bar?a=b//c", URIUtil.compactPath("//foo//bar?a=b//c")); + Arguments.of("", null, ""), + Arguments.of("", "", ""), + Arguments.of("", "bbb", "bbb"), + Arguments.of("", "/", "/"), + Arguments.of("", "/bbb", "/bbb"), - assertEquals("/foo/bar", URIUtil.compactPath("/foo///bar")); - assertEquals("/foo/bar?a=b//c", URIUtil.compactPath("/foo///bar?a=b//c")); + Arguments.of("aaa", null, "aaa"), + Arguments.of("aaa", "", "aaa"), + Arguments.of("aaa", "bbb", "aaa/bbb"), + Arguments.of("aaa", "/", "aaa/"), + Arguments.of("aaa", "/bbb", "aaa/bbb"), + + Arguments.of("/", null, "/"), + Arguments.of("/", "", "/"), + Arguments.of("/", "bbb", "/bbb"), + Arguments.of("/", "/", "/"), + Arguments.of("/", "/bbb", "/bbb"), + + Arguments.of("aaa/", null, "aaa/"), + Arguments.of("aaa/", "", "aaa/"), + Arguments.of("aaa/", "bbb", "aaa/bbb"), + Arguments.of("aaa/", "/", "aaa/"), + Arguments.of("aaa/", "/bbb", "aaa/bbb"), + + Arguments.of(";JS", null, ";JS"), + Arguments.of(";JS", "", ";JS"), + Arguments.of(";JS", "bbb", "bbb;JS"), + Arguments.of(";JS", "/", "/;JS"), + Arguments.of(";JS", "/bbb", "/bbb;JS"), + + Arguments.of("aaa;JS", null, "aaa;JS"), + Arguments.of("aaa;JS", "", "aaa;JS"), + Arguments.of("aaa;JS", "bbb", "aaa/bbb;JS"), + Arguments.of("aaa;JS", "/", "aaa/;JS"), + Arguments.of("aaa;JS", "/bbb", "aaa/bbb;JS"), + + Arguments.of("aaa/;JS", null, "aaa/;JS"), + Arguments.of("aaa/;JS", "", "aaa/;JS"), + Arguments.of("aaa/;JS", "bbb", "aaa/bbb;JS"), + Arguments.of("aaa/;JS", "/", "aaa/;JS"), + Arguments.of("aaa/;JS", "/bbb", "aaa/bbb;JS"), + + Arguments.of("?A=1", null, "?A=1"), + Arguments.of("?A=1", "", "?A=1"), + Arguments.of("?A=1", "bbb", "bbb?A=1"), + Arguments.of("?A=1", "/", "/?A=1"), + Arguments.of("?A=1", "/bbb", "/bbb?A=1"), + + Arguments.of("aaa?A=1", null, "aaa?A=1"), + Arguments.of("aaa?A=1", "", "aaa?A=1"), + Arguments.of("aaa?A=1", "bbb", "aaa/bbb?A=1"), + Arguments.of("aaa?A=1", "/", "aaa/?A=1"), + Arguments.of("aaa?A=1", "/bbb", "aaa/bbb?A=1"), + + Arguments.of("aaa/?A=1", null, "aaa/?A=1"), + Arguments.of("aaa/?A=1", "", "aaa/?A=1"), + Arguments.of("aaa/?A=1", "bbb", "aaa/bbb?A=1"), + Arguments.of("aaa/?A=1", "/", "aaa/?A=1"), + Arguments.of("aaa/?A=1", "/bbb", "aaa/bbb?A=1"), + + Arguments.of(";JS?A=1", null, ";JS?A=1"), + Arguments.of(";JS?A=1", "", ";JS?A=1"), + Arguments.of(";JS?A=1", "bbb", "bbb;JS?A=1"), + Arguments.of(";JS?A=1", "/", "/;JS?A=1"), + Arguments.of(";JS?A=1", "/bbb", "/bbb;JS?A=1"), + + Arguments.of("aaa;JS?A=1", null, "aaa;JS?A=1"), + Arguments.of("aaa;JS?A=1", "", "aaa;JS?A=1"), + Arguments.of("aaa;JS?A=1", "bbb", "aaa/bbb;JS?A=1"), + Arguments.of("aaa;JS?A=1", "/", "aaa/;JS?A=1"), + Arguments.of("aaa;JS?A=1", "/bbb", "aaa/bbb;JS?A=1"), + + Arguments.of("aaa/;JS?A=1", null, "aaa/;JS?A=1"), + Arguments.of("aaa/;JS?A=1", "", "aaa/;JS?A=1"), + Arguments.of("aaa/;JS?A=1", "bbb", "aaa/bbb;JS?A=1"), + Arguments.of("aaa/;JS?A=1", "/", "aaa/;JS?A=1"), + Arguments.of("aaa/;JS?A=1", "/bbb", "aaa/bbb;JS?A=1") + ); } - @Test // TODO: Parameterize - public void testParentPath() + @ParameterizedTest(name = "[{index}] {0}+{1}") + @MethodSource("addEncodedPathsSource") + public void testAddEncodedPaths(String path1, String path2, String expected) { - assertEquals("/aaa/", URIUtil.parentPath("/aaa/bbb/"), "parent /aaa/bbb/"); - assertEquals("/aaa/", URIUtil.parentPath("/aaa/bbb"), "parent /aaa/bbb"); - assertEquals("/", URIUtil.parentPath("/aaa/"), "parent /aaa/"); - assertEquals("/", URIUtil.parentPath("/aaa"), "parent /aaa"); - assertEquals(null, URIUtil.parentPath("/"), "parent /"); - assertEquals(null, URIUtil.parentPath(null), "parent null"); + String actual = URIUtil.addEncodedPaths(path1, path2); + assertEquals(expected, actual, String.format("%s+%s", path1, path2)); } - @Test // TODO: Parameterize - public void testEqualsIgnoreEncoding() + public static Stream<Arguments> addDecodedPathsSource() { - assertTrue(URIUtil.equalsIgnoreEncodings("http://example.com/foo/bar", "http://example.com/foo/bar")); - assertTrue(URIUtil.equalsIgnoreEncodings("/barry's", "/barry%27s")); - assertTrue(URIUtil.equalsIgnoreEncodings("/barry%27s", "/barry's")); - assertTrue(URIUtil.equalsIgnoreEncodings("/barry%27s", "/barry%27s")); - assertTrue(URIUtil.equalsIgnoreEncodings("/b rry's", "/b%20rry%27s")); - assertTrue(URIUtil.equalsIgnoreEncodings("/b rry%27s", "/b%20rry's")); - assertTrue(URIUtil.equalsIgnoreEncodings("/b rry%27s", "/b%20rry%27s")); + return Stream.of( + Arguments.of(null, null, null), + Arguments.of(null, "", ""), + Arguments.of(null, "bbb", "bbb"), + Arguments.of(null, "/", "/"), + Arguments.of(null, "/bbb", "/bbb"), - assertTrue(URIUtil.equalsIgnoreEncodings("/foo%2fbar", "/foo%2fbar")); - assertTrue(URIUtil.equalsIgnoreEncodings("/foo%2fbar", "/foo%2Fbar")); + Arguments.of("", null, ""), + Arguments.of("", "", ""), + Arguments.of("", "bbb", "bbb"), + Arguments.of("", "/", "/"), + Arguments.of("", "/bbb", "/bbb"), - assertFalse(URIUtil.equalsIgnoreEncodings("ABC", "abc")); - assertFalse(URIUtil.equalsIgnoreEncodings("/barry's", "/barry%26s")); + Arguments.of("aaa", null, "aaa"), + Arguments.of("aaa", "", "aaa"), + Arguments.of("aaa", "bbb", "aaa/bbb"), + Arguments.of("aaa", "/", "aaa/"), + Arguments.of("aaa", "/bbb", "aaa/bbb"), - assertFalse(URIUtil.equalsIgnoreEncodings("/foo/bar", "/foo%2fbar")); - assertFalse(URIUtil.equalsIgnoreEncodings("/foo2fbar", "/foo/bar")); + Arguments.of("/", null, "/"), + Arguments.of("/", "", "/"), + Arguments.of("/", "bbb", "/bbb"), + Arguments.of("/", "/", "/"), + Arguments.of("/", "/bbb", "/bbb"), + + Arguments.of("aaa/", null, "aaa/"), + Arguments.of("aaa/", "", "aaa/"), + Arguments.of("aaa/", "bbb", "aaa/bbb"), + Arguments.of("aaa/", "/", "aaa/"), + Arguments.of("aaa/", "/bbb", "aaa/bbb"), + + Arguments.of(";JS", null, ";JS"), + Arguments.of(";JS", "", ";JS"), + Arguments.of(";JS", "bbb", ";JS/bbb"), + Arguments.of(";JS", "/", ";JS/"), + Arguments.of(";JS", "/bbb", ";JS/bbb"), + + Arguments.of("aaa;JS", null, "aaa;JS"), + Arguments.of("aaa;JS", "", "aaa;JS"), + Arguments.of("aaa;JS", "bbb", "aaa;JS/bbb"), + Arguments.of("aaa;JS", "/", "aaa;JS/"), + Arguments.of("aaa;JS", "/bbb", "aaa;JS/bbb"), + + Arguments.of("aaa/;JS", null, "aaa/;JS"), + Arguments.of("aaa/;JS", "", "aaa/;JS"), + Arguments.of("aaa/;JS", "bbb", "aaa/;JS/bbb"), + Arguments.of("aaa/;JS", "/", "aaa/;JS/"), + Arguments.of("aaa/;JS", "/bbb", "aaa/;JS/bbb"), + + Arguments.of("?A=1", null, "?A=1"), + Arguments.of("?A=1", "", "?A=1"), + Arguments.of("?A=1", "bbb", "?A=1/bbb"), + Arguments.of("?A=1", "/", "?A=1/"), + Arguments.of("?A=1", "/bbb", "?A=1/bbb"), + + Arguments.of("aaa?A=1", null, "aaa?A=1"), + Arguments.of("aaa?A=1", "", "aaa?A=1"), + Arguments.of("aaa?A=1", "bbb", "aaa?A=1/bbb"), + Arguments.of("aaa?A=1", "/", "aaa?A=1/"), + Arguments.of("aaa?A=1", "/bbb", "aaa?A=1/bbb"), + + Arguments.of("aaa/?A=1", null, "aaa/?A=1"), + Arguments.of("aaa/?A=1", "", "aaa/?A=1"), + Arguments.of("aaa/?A=1", "bbb", "aaa/?A=1/bbb"), + Arguments.of("aaa/?A=1", "/", "aaa/?A=1/"), + Arguments.of("aaa/?A=1", "/bbb", "aaa/?A=1/bbb") + ); } - @Test // TODO: Parameterize - public void testEqualsIgnoreEncoding_JarFile() + @ParameterizedTest(name = "[{index}] {0}+{1}") + @MethodSource("addDecodedPathsSource") + public void testAddDecodedPaths(String path1, String path2, String expected) { - URI uriA = URI.create("jar:file:/path/to/main.jar!/META-INF/versions/"); - URI uriB = URI.create("jar:file:/path/to/main.jar!/META-INF/%76ersions/"); - assertTrue(URIUtil.equalsIgnoreEncodings(uriA, uriB)); - - uriA = URI.create("JAR:FILE:/path/to/main.jar!/META-INF/versions/"); - uriB = URI.create("jar:file:/path/to/main.jar!/META-INF/versions/"); - assertTrue(URIUtil.equalsIgnoreEncodings(uriA, uriB)); + String actual = URIUtil.addPaths(path1, path2); + assertEquals(expected, actual, String.format("%s+%s", path1, path2)); } - @Test // TODO: Parameterize - public void testJarSource() throws Exception + public static Stream<Arguments> compactPathSource() { - assertThat(URIUtil.getJarSource("file:///tmp/"), is("file:///tmp/")); - assertThat(URIUtil.getJarSource("jar:file:///tmp/foo.jar"), is("file:///tmp/foo.jar")); - assertThat(URIUtil.getJarSource("jar:file:///tmp/foo.jar!/some/path"), is("file:///tmp/foo.jar")); - assertThat(URIUtil.getJarSource(new URI("file:///tmp/")), is(new URI("file:///tmp/"))); - assertThat(URIUtil.getJarSource(new URI("jar:file:///tmp/foo.jar")), is(new URI("file:///tmp/foo.jar"))); - assertThat(URIUtil.getJarSource(new URI("jar:file:///tmp/foo.jar!/some/path")), is(new URI("file:///tmp/foo.jar"))); + return Stream.of( + Arguments.of("/foo/bar", "/foo/bar"), + Arguments.of("/foo/bar?a=b//c", "/foo/bar?a=b//c"), + + Arguments.of("//foo//bar", "/foo/bar"), + Arguments.of("//foo//bar?a=b//c", "/foo/bar?a=b//c"), + + Arguments.of("/foo///bar", "/foo/bar"), + Arguments.of("/foo///bar?a=b//c", "/foo/bar?a=b//c") + ); } - public static Stream<String[]> encodeSpaces() + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("compactPathSource") + public void testCompactPath(String path, String expected) { - List<String[]> data = new ArrayList<>(); + String actual = URIUtil.compactPath(path); + assertEquals(expected, actual); + } - // [raw, expected] + public static Stream<Arguments> parentPathSource() + { + return Stream.of( + Arguments.of("/aaa/bbb/", "/aaa/"), + Arguments.of("/aaa/bbb", "/aaa/"), + Arguments.of("/aaa/", "/"), + Arguments.of("/aaa", "/"), + Arguments.of("/", null), + Arguments.of(null, null) + ); + } - // null - data.add(new String[]{null, null}); + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("parentPathSource") + public void testParentPath(String path, String expectedPath) + { + String actual = URIUtil.parentPath(path); + assertEquals(expectedPath, actual, String.format("parent %s", path)); + } - // no spaces - data.add(new String[]{"abc", "abc"}); + public static Stream<Arguments> equalsIgnoreEncodingStringTrueSource() + { + return Stream.of( + Arguments.of("http://example.com/foo/bar", "http://example.com/foo/bar"), + Arguments.of("/barry's", "/barry%27s"), + Arguments.of("/barry%27s", "/barry's"), + Arguments.of("/barry%27s", "/barry%27s"), + Arguments.of("/b rry's", "/b%20rry%27s"), + Arguments.of("/b rry%27s", "/b%20rry's"), + Arguments.of("/b rry%27s", "/b%20rry%27s"), - // match - data.add(new String[]{"a c", "a%20c"}); - data.add(new String[]{" ", "%20%20%20"}); - data.add(new String[]{"a%20space", "a%20space"}); - - return data.stream(); + Arguments.of("/foo%2fbar", "/foo%2fbar"), + Arguments.of("/foo%2fbar", "/foo%2Fbar") + ); } @ParameterizedTest - @MethodSource(value = "encodeSpaces") + @MethodSource("equalsIgnoreEncodingStringTrueSource") + public void testEqualsIgnoreEncodingStringTrue(String uriA, String uriB) + { + assertTrue(URIUtil.equalsIgnoreEncodings(uriA, uriB)); + } + + public static Stream<Arguments> equalsIgnoreEncodingStringFalseSource() + { + return Stream.of( + Arguments.of("ABC", "abc"), + Arguments.of("/barry's", "/barry%26s"), + + Arguments.of("/foo/bar", "/foo%2fbar"), + Arguments.of("/foo2fbar", "/foo/bar") + ); + } + + @ParameterizedTest + @MethodSource("equalsIgnoreEncodingStringFalseSource") + public void testEqualsIgnoreEncodingStringFalse(String uriA, String uriB) + { + assertFalse(URIUtil.equalsIgnoreEncodings(uriA, uriB)); + } + + public static Stream<Arguments> equalsIgnoreEncodingURITrueSource() + { + return Stream.of( + Arguments.of( + URI.create("jar:file:/path/to/main.jar!/META-INF/versions/"), + URI.create("jar:file:/path/to/main.jar!/META-INF/%76ersions/") + ), + Arguments.of( + URI.create("JAR:FILE:/path/to/main.jar!/META-INF/versions/"), + URI.create("jar:file:/path/to/main.jar!/META-INF/versions/") + ) + ); + } + + @ParameterizedTest + @MethodSource("equalsIgnoreEncodingURITrueSource") + public void testEqualsIgnoreEncodingURITrue(URI uriA, URI uriB) + { + assertTrue(URIUtil.equalsIgnoreEncodings(uriA, uriB)); + } + + public static Stream<Arguments> getJarSourceStringSource() + { + return Stream.of( + Arguments.of("file:///tmp/", "file:///tmp/"), + Arguments.of("jar:file:///tmp/foo.jar", "file:///tmp/foo.jar"), + Arguments.of("jar:file:///tmp/foo.jar!/some/path", "file:///tmp/foo.jar") + ); + } + + @ParameterizedTest + @MethodSource("getJarSourceStringSource") + public void testJarSourceString(String uri, String expectedJarUri) throws Exception + { + assertThat(URIUtil.getJarSource(uri), is(expectedJarUri)); + } + + public static Stream<Arguments> getJarSourceURISource() + { + return Stream.of( + Arguments.of(URI.create("file:///tmp/"), URI.create("file:///tmp/")), + Arguments.of(URI.create("jar:file:///tmp/foo.jar"), URI.create("file:///tmp/foo.jar")), + Arguments.of(URI.create("jar:file:///tmp/foo.jar!/some/path"), URI.create("file:///tmp/foo.jar")) + ); + } + + @ParameterizedTest + @MethodSource("getJarSourceURISource") + public void testJarSourceURI(URI uri, URI expectedJarUri) throws Exception + { + assertThat(URIUtil.getJarSource(uri), is(expectedJarUri)); + } + + public static Stream<Arguments> encodeSpacesSource() + { + return Stream.of( + // null + Arguments.of(null, null), + + // no spaces + Arguments.of("abc", "abc"), + + // match + Arguments.of("a c", "a%20c"), + Arguments.of(" ", "%20%20%20"), + Arguments.of("a%20space", "a%20space") + ); + } + + @ParameterizedTest + @MethodSource("encodeSpacesSource") public void testEncodeSpaces(String raw, String expected) { assertThat(URIUtil.encodeSpaces(raw), is(expected)); } - public static Stream<String[]> encodeSpecific() + public static Stream<Arguments> encodeSpecific() { - List<String[]> data = new ArrayList<>(); + return Stream.of( + // [raw, chars, expected] - // [raw, chars, expected] + // null input + Arguments.of(null, null, null), - // null input - data.add(new String[]{null, null, null}); + // null chars + Arguments.of("abc", null, "abc"), - // null chars - data.add(new String[]{"abc", null, "abc"}); + // empty chars + Arguments.of("abc", "", "abc"), - // empty chars - data.add(new String[]{"abc", "", "abc"}); + // no matches + Arguments.of("abc", ".;", "abc"), + Arguments.of("xyz", ".;", "xyz"), + Arguments.of(":::", ".;", ":::"), - // no matches - data.add(new String[]{"abc", ".;", "abc"}); - data.add(new String[]{"xyz", ".;", "xyz"}); - data.add(new String[]{":::", ".;", ":::"}); + // matches + Arguments.of("a c", " ", "a%20c"), + Arguments.of("name=value", "=", "name%3Dvalue"), + Arguments.of("This has fewer then 10% hits.", ".%", "This has fewer then 10%25 hits%2E"), - // matches - data.add(new String[]{"a c", " ", "a%20c"}); - data.add(new String[]{"name=value", "=", "name%3Dvalue"}); - data.add(new String[]{"This has fewer then 10% hits.", ".%", "This has fewer then 10%25 hits%2E"}); - - // partially encoded already - data.add(new String[]{"a%20name=value%20pair", "=", "a%20name%3Dvalue%20pair"}); - data.add(new String[]{"a%20name=value%20pair", "=%", "a%2520name%3Dvalue%2520pair"}); - - return data.stream(); + // partially encoded already + Arguments.of("a%20name=value%20pair", "=", "a%20name%3Dvalue%20pair"), + Arguments.of("a%20name=value%20pair", "=%", "a%2520name%3Dvalue%2520pair") + ); } @ParameterizedTest @@ -382,36 +486,34 @@ public class URIUtilTest assertThat(URIUtil.encodeSpecific(raw, chars), is(expected)); } - public static Stream<String[]> decodeSpecific() + public static Stream<Arguments> decodeSpecific() { - List<String[]> data = new ArrayList<>(); + return Stream.of( + // [raw, chars, expected] - // [raw, chars, expected] + // null input + Arguments.of(null, null, null), - // null input - data.add(new String[]{null, null, null}); + // null chars + Arguments.of("abc", null, "abc"), - // null chars - data.add(new String[]{"abc", null, "abc"}); + // empty chars + Arguments.of("abc", "", "abc"), - // empty chars - data.add(new String[]{"abc", "", "abc"}); + // no matches + Arguments.of("abc", ".;", "abc"), + Arguments.of("xyz", ".;", "xyz"), + Arguments.of(":::", ".;", ":::"), - // no matches - data.add(new String[]{"abc", ".;", "abc"}); - data.add(new String[]{"xyz", ".;", "xyz"}); - data.add(new String[]{":::", ".;", ":::"}); + // matches + Arguments.of("a%20c", " ", "a c"), + Arguments.of("name%3Dvalue", "=", "name=value"), + Arguments.of("This has fewer then 10%25 hits%2E", ".%", "This has fewer then 10% hits."), - // matches - data.add(new String[]{"a%20c", " ", "a c"}); - data.add(new String[]{"name%3Dvalue", "=", "name=value"}); - data.add(new String[]{"This has fewer then 10%25 hits%2E", ".%", "This has fewer then 10% hits."}); - - // partially decode - data.add(new String[]{"a%20name%3Dvalue%20pair", "=", "a%20name=value%20pair"}); - data.add(new String[]{"a%2520name%3Dvalue%2520pair", "=%", "a%20name=value%20pair"}); - - return data.stream(); + // partially decode + Arguments.of("a%20name%3Dvalue%20pair", "=", "a%20name=value%20pair"), + Arguments.of("a%2520name%3Dvalue%2520pair", "=%", "a%20name=value%20pair") + ); } @ParameterizedTest From 7da57151ed31bab515a312e81249743a69ada90c Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt <joakim.erdfelt@gmail.com> Date: Wed, 28 Aug 2019 12:08:18 -0500 Subject: [PATCH 49/55] Issue #4033 - lenient percent decode in URIUtil + Allows for preserving decoded Strings like "X%YZ" Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com> --- .../org/eclipse/jetty/util/StringUtil.java | 15 +++ .../java/org/eclipse/jetty/util/URIUtil.java | 107 ++++++++++++++++-- .../org/eclipse/jetty/util/URIUtilTest.java | 31 ++++- 3 files changed, 138 insertions(+), 15 deletions(-) diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java b/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java index 2c466a34b55..fbc96e38c1d 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java @@ -644,6 +644,21 @@ public class StringUtil return __UTF8.equalsIgnoreCase(charset) || __UTF8.equalsIgnoreCase(normalizeCharset(charset)); } + public static boolean isHex(String str, int offset, int length) + { + for (int i = offset; i < (offset + length); i++) + { + char c = str.charAt(i); + if (!(((c >= 'a') && (c <= 'f')) || + ((c >= 'A') && (c <= 'F')) || + ((c >= '0') && (c <= '9')))) + { + return false; + } + } + return true; + } + public static String printable(String name) { if (name == null) diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java b/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java index b86a1b7958a..9e9b2c536dd 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java @@ -470,24 +470,72 @@ public class URIUtil builder = new Utf8StringBuilder(path.length()); builder.append(path, offset, i - offset); } - if ((i + 2) < end) + + // lenient percent decoding + if (i >= end) { - char u = path.charAt(i + 1); - if (u == 'u') + // [LENIENT] a percent sign at end of string. + builder.append('%'); + i = end; + } + else if (end > (i + 1)) + { + char type = path.charAt(i + 1); + if (type == 'u') { - // TODO this is wrong. This is a codepoint not a char - builder.append((char)(0xffff & TypeUtil.parseInt(path, i + 2, 4, 16))); - i += 5; + // We have a possible (deprecated) microsoft unicode code point "%u####" + // - not recommended to use as it's limited to 2 bytes. + if ((i + 5) >= end) + { + // [LENIENT] we have a partial "%u####" at the end of a string. + builder.append(path, i, (end - i)); + i = end; + } + else + { + // this seems wrong, as we are casting to a char, but that's the known + // limitation of this deprecated encoding (only 2 bytes allowed) + if (StringUtil.isHex(path, i + 2, 4)) + { + builder.append((char)(0xffff & TypeUtil.parseInt(path, i + 2, 4, 16))); + i += 5; + } + else + { + // [LENIENT] copy the "%u" as-is. + builder.append(path, i, 2); + i += 1; + } + } + } + else if (end > (i + 2)) + { + // we have a possible "%##" encoding + if (StringUtil.isHex(path, i + 1, 2)) + { + char c1 = path.charAt(i + 1); + char c2 = path.charAt(i + 2); + builder.append((byte)(0xff & (TypeUtil.convertHexDigit(c1) * 16 + TypeUtil.convertHexDigit(c2)))); + i += 2; + } + else + { + builder.append(path, i, 3); + i += 2; + } } else { - builder.append((byte)(0xff & (TypeUtil.convertHexDigit(u) * 16 + TypeUtil.convertHexDigit(path.charAt(i + 2))))); - i += 2; + // [LENIENT] incomplete "%##" sequence at end of string + builder.append(path, i, (end - i)); + i = end; } } else { - throw new IllegalArgumentException("Bad URI % encoding"); + // [LENIENT] the "%" at the end of the string + builder.append(path, i, (end - i)); + i = end; } break; @@ -1156,12 +1204,32 @@ public class URIUtil int oa = uriA.charAt(a++); int ca = oa; if (ca == '%') - ca = TypeUtil.convertHexDigit(uriA.charAt(a++)) * 16 + TypeUtil.convertHexDigit(uriA.charAt(a++)); + { + ca = lenientPercentDecode(uriA, a); + if (ca == (-1)) + { + ca = '%'; + } + else + { + a += 2; + } + } int ob = uriB.charAt(b++); int cb = ob; if (cb == '%') - cb = TypeUtil.convertHexDigit(uriB.charAt(b++)) * 16 + TypeUtil.convertHexDigit(uriB.charAt(b++)); + { + cb = lenientPercentDecode(uriB, b); + if (cb == (-1)) + { + cb = '%'; + } + else + { + b += 2; + } + } if (ca == '/' && oa != ob) return false; @@ -1172,6 +1240,23 @@ public class URIUtil return a == lenA && b == lenB; } + private static int lenientPercentDecode(String str, int offset) + { + if (offset >= str.length()) + return -1; + + char a1 = str.charAt(offset); + char a2 = str.charAt(offset + 1); + try + { + return TypeUtil.convertHexDigit(a1) * 16 + TypeUtil.convertHexDigit(a2); + } + catch (NumberFormatException e) + { + return -1; + } + } + public static boolean equalsIgnoreEncodings(URI uriA, URI uriB) { if (uriA.equals(uriB)) diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilTest.java index 0ea76fcef5f..7e7a1aa628a 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilTest.java @@ -90,9 +90,22 @@ public class URIUtilTest // Test for null character (real world ugly test case) byte[] oddBytes = {'/', 0x00, '/'}; String odd = new String(oddBytes, StandardCharsets.ISO_8859_1); - assertEquals(odd, URIUtil.decodePath("/%00/")); arguments.add(Arguments.of("/%00/", odd)); + // Deprecated Microsoft Percent-U encoding + arguments.add(Arguments.of("abc%u3040", "abc\u3040")); + + // Lenient decode + arguments.add(Arguments.of("abc%xyz", "abc%xyz")); // not a "%##" + arguments.add(Arguments.of("abc%", "abc%")); // percent at end of string + arguments.add(Arguments.of("abc%A", "abc%A")); // incomplete "%##" at end of string + arguments.add(Arguments.of("abc%uvwxyz", "abc%uvwxyz")); // not a valid "%u####" + arguments.add(Arguments.of("abc%uEFGHIJ", "abc%uEFGHIJ")); // not a valid "%u####" + arguments.add(Arguments.of("abc%uABC", "abc%uABC")); // incomplete "%u####" + arguments.add(Arguments.of("abc%uAB", "abc%uAB")); // incomplete "%u####" + arguments.add(Arguments.of("abc%uA", "abc%uA")); // incomplete "%u####" + arguments.add(Arguments.of("abc%u", "abc%u")); // incomplete "%u####" + return arguments.stream(); } @@ -344,7 +357,11 @@ public class URIUtilTest Arguments.of("/b rry%27s", "/b%20rry%27s"), Arguments.of("/foo%2fbar", "/foo%2fbar"), - Arguments.of("/foo%2fbar", "/foo%2Fbar") + Arguments.of("/foo%2fbar", "/foo%2Fbar"), + + // encoded vs not-encode ("%" symbol is encoded as "%25") + Arguments.of("/abc%25xyz", "/abc%xyz"), + Arguments.of("/zzz%25", "/zzz%") ); } @@ -358,11 +375,17 @@ public class URIUtilTest public static Stream<Arguments> equalsIgnoreEncodingStringFalseSource() { return Stream.of( + // case difference Arguments.of("ABC", "abc"), + // Encoding difference ("'" is "%27") Arguments.of("/barry's", "/barry%26s"), - + // Never match on "%2f" differences Arguments.of("/foo/bar", "/foo%2fbar"), - Arguments.of("/foo2fbar", "/foo/bar") + // not actually encoded + Arguments.of("/foo2fbar", "/foo/bar"), + // encoded vs not-encode ("%" symbol is encoded as "%25") + Arguments.of("/yyy%25zzz", "/aaa%xxx"), + Arguments.of("/zzz%25", "/aaa%") ); } From f47115c5856921728954cf5c24de106753ee0a0c Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt <joakim.erdfelt@gmail.com> Date: Wed, 28 Aug 2019 12:20:52 -0500 Subject: [PATCH 50/55] Issue #4033 - More tests for Lenient URIUtil behavior Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com> --- .../main/java/org/eclipse/jetty/util/StringUtil.java | 5 +++++ .../main/java/org/eclipse/jetty/util/URIUtil.java | 12 ++++-------- .../java/org/eclipse/jetty/util/URIUtilTest.java | 2 ++ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java b/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java index fbc96e38c1d..2b01724eb82 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java @@ -646,6 +646,11 @@ public class StringUtil public static boolean isHex(String str, int offset, int length) { + if (offset + length > str.length()) + { + return false; + } + for (int i = offset; i < (offset + length); i++) { char c = str.charAt(i); diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java b/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java index 9e9b2c536dd..63c4b081162 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java @@ -513,9 +513,7 @@ public class URIUtil // we have a possible "%##" encoding if (StringUtil.isHex(path, i + 1, 2)) { - char c1 = path.charAt(i + 1); - char c2 = path.charAt(i + 2); - builder.append((byte)(0xff & (TypeUtil.convertHexDigit(c1) * 16 + TypeUtil.convertHexDigit(c2)))); + builder.append((byte)TypeUtil.parseInt(path, i + 1, 2, 16)); i += 2; } else @@ -1245,13 +1243,11 @@ public class URIUtil if (offset >= str.length()) return -1; - char a1 = str.charAt(offset); - char a2 = str.charAt(offset + 1); - try + if (StringUtil.isHex(str, offset, 2)) { - return TypeUtil.convertHexDigit(a1) * 16 + TypeUtil.convertHexDigit(a2); + return TypeUtil.parseInt(str, offset, 2, 16); } - catch (NumberFormatException e) + else { return -1; } diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilTest.java index 7e7a1aa628a..ce02b8019bb 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilTest.java @@ -361,6 +361,8 @@ public class URIUtilTest // encoded vs not-encode ("%" symbol is encoded as "%25") Arguments.of("/abc%25xyz", "/abc%xyz"), + Arguments.of("/abc%25xy", "/abc%xy"), + Arguments.of("/abc%25x", "/abc%x"), Arguments.of("/zzz%25", "/zzz%") ); } From e56d91196d6a669b04dc499cfc56329cbccd6297 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt <joakim.erdfelt@gmail.com> Date: Wed, 28 Aug 2019 12:31:33 -0500 Subject: [PATCH 51/55] Issue #4020 - Adding JMX to BrowserDebugTool to test dump Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com> --- jetty-websocket/websocket-server/pom.xml | 6 ++++++ .../jetty/websocket/server/browser/BrowserDebugTool.java | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/jetty-websocket/websocket-server/pom.xml b/jetty-websocket/websocket-server/pom.xml index e406f52595c..c3e50f78364 100644 --- a/jetty-websocket/websocket-server/pom.xml +++ b/jetty-websocket/websocket-server/pom.xml @@ -107,6 +107,12 @@ <classifier>tests</classifier> <scope>test</scope> </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-jmx</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> <dependency> <groupId>org.eclipse.jetty.toolchain</groupId> <artifactId>jetty-test-helper</artifactId> diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/browser/BrowserDebugTool.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/browser/BrowserDebugTool.java index 751e5b5479e..2cd9a7fb9f6 100644 --- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/browser/BrowserDebugTool.java +++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/browser/BrowserDebugTool.java @@ -20,12 +20,14 @@ package org.eclipse.jetty.websocket.server.browser; import java.io.FileNotFoundException; import java.io.IOException; +import java.lang.management.ManagementFactory; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import org.eclipse.jetty.jmx.MBeanContainer; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.ResourceHandler; @@ -131,10 +133,17 @@ public class BrowserDebugTool implements WebSocketCreator public void prepare(int port) throws IOException, URISyntaxException { server = new Server(); + + // Setup JMX + MBeanContainer mbContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer()); + server.addBean(mbContainer, true); + + // Setup Connector connector = new ServerConnector(server); connector.setPort(port); server.addConnector(connector); + // Setup WebSocket WebSocketHandler wsHandler = new WebSocketHandler() { @Override From 37e7884382dadf9feb7092ed928e2134b5aa3df2 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt <joakim.erdfelt@gmail.com> Date: Wed, 28 Aug 2019 12:36:07 -0500 Subject: [PATCH 52/55] Issue #4020 - Applying change requested from PR review Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com> --- .../extensions/WebSocketExtensionFactory.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/WebSocketExtensionFactory.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/WebSocketExtensionFactory.java index f1ca84d635f..118f7a8a4cd 100644 --- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/WebSocketExtensionFactory.java +++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/WebSocketExtensionFactory.java @@ -51,12 +51,19 @@ public class WebSocketExtensionFactory extends ExtensionFactory implements LifeC public WebSocketExtensionFactory(WebSocketContainerScope container) { - containerLifeCycle = new ContainerLifeCycle(); + containerLifeCycle = new ContainerLifeCycle() + { + @Override + public String toString() + { + return String.format("%s@%x{%s}", WebSocketExtensionFactory.class.getSimpleName(), hashCode(), containerLifeCycle.getState()); + } + }; availableExtensions = new HashMap<>(); for (Extension ext : extensionLoader) { if (ext != null) - availableExtensions.put(ext.getName(),ext.getClass()); + availableExtensions.put(ext.getName(), ext.getClass()); } this.container = container; @@ -135,7 +142,7 @@ public class WebSocketExtensionFactory extends ExtensionFactory implements LifeC @Override public void register(String name, Class<? extends Extension> extension) { - availableExtensions.put(name,extension); + availableExtensions.put(name, extension); } @Override @@ -233,6 +240,6 @@ public class WebSocketExtensionFactory extends ExtensionFactory implements LifeC @Override public String toString() { - return String.format("%s@%x{%s}", WebSocketExtensionFactory.class.getName(), hashCode(), containerLifeCycle.getState()); + return containerLifeCycle.toString(); } } From 2fcb311c568e4f288d2cfdbd191027e7e37b98b2 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt <joakim.erdfelt@gmail.com> Date: Wed, 28 Aug 2019 16:32:19 -0500 Subject: [PATCH 53/55] Issue #4033 - Addressing Lenient URIUtil decode behavior change in test Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com> --- .../jetty/server/HttpConnectionTest.java | 17 ++++++----------- .../java/org/eclipse/jetty/util/URIUtil.java | 3 ++- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java index f2be892780c..408ebb1535a 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java @@ -690,17 +690,12 @@ public class HttpConnectionTest @Test public void testBadURIencoding() throws Exception { - Log.getLogger(HttpParser.class).info("badMessage: bad encoding expected ..."); - String response; - - try (StacklessLogging stackless = new StacklessLogging(HttpParser.class)) - { - response = connector.getResponse("GET /bad/encoding%1 HTTP/1.1\r\n" + - "Host: localhost\r\n" + - "Connection: close\r\n" + - "\r\n"); - checkContains(response, 0, "HTTP/1.1 400"); - } + // The URI is being leniently decoded, leaving the "%x" alone + String response = connector.getResponse("GET /bad/encoding%x HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Connection: close\r\n" + + "\r\n"); + checkContains(response, 0, "HTTP/1.1 200"); } @Test diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java b/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java index 63c4b081162..73a3e77e705 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java @@ -1229,11 +1229,12 @@ public class URIUtil } } + // Don't match on encoded slash if (ca == '/' && oa != ob) return false; if (ca != cb) - return URIUtil.decodePath(uriA).equals(URIUtil.decodePath(uriB)); + return false; } return a == lenA && b == lenB; } From bb7eb4bc86d6e7e3df0ad77bd7efa1861a1866aa Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt <joakim.erdfelt@gmail.com> Date: Fri, 30 Aug 2019 09:23:34 -0500 Subject: [PATCH 54/55] Adding some comments to URIUtilTest Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com> --- .../src/test/java/org/eclipse/jetty/util/URIUtilTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilTest.java index ce02b8019bb..1354b2e3398 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilTest.java @@ -381,7 +381,8 @@ public class URIUtilTest Arguments.of("ABC", "abc"), // Encoding difference ("'" is "%27") Arguments.of("/barry's", "/barry%26s"), - // Never match on "%2f" differences + // Never match on "%2f" differences - only intested in filename / directory name differences + // This could be a directory called "foo" with a file called "bar" on the left, and just a file "foo%2fbar" on the right Arguments.of("/foo/bar", "/foo%2fbar"), // not actually encoded Arguments.of("/foo2fbar", "/foo/bar"), From 49ba6d1acb230ecc5d2cdca1517d3e97dff4bfbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BA=B7=E6=99=BA=E5=86=AC?= <worldkzd@gmail.com> Date: Tue, 3 Sep 2019 02:29:50 +0800 Subject: [PATCH 55/55] fix typo and grammar (#4045) Signed-off-by: KangZhiDong <worldkzd@gmail.com> --- .../TestSecurityAnnotationConversions.java | 2 +- .../eclipse/jetty/client/HttpClientTransport.java | 2 +- .../java/org/eclipse/jetty/client/HttpContent.java | 2 +- .../org/eclipse/jetty/client/HttpReceiver.java | 2 +- .../org/eclipse/jetty/client/HttpRedirector.java | 2 +- .../java/org/eclipse/jetty/client/HttpRequest.java | 2 +- .../java/org/eclipse/jetty/client/api/Request.java | 2 +- .../org/eclipse/jetty/client/api/Response.java | 2 +- .../jetty/client/http/HttpReceiverOverHTTP.java | 2 +- .../jetty/client/HttpConnectionLifecycleTest.java | 2 +- .../eclipse/jetty/continuation/Continuation.java | 2 +- jetty-deploy/src/test/resources/jetty-http.xml | 4 ++-- .../fcgi/server/proxy/FastCGIProxyServlet.java | 2 +- .../org/eclipse/jetty/http/HostPortHttpField.java | 2 +- .../java/org/eclipse/jetty/http/HttpField.java | 2 +- .../java/org/eclipse/jetty/http/HttpMethod.java | 4 ++-- .../java/org/eclipse/jetty/http/HttpParser.java | 8 ++++---- .../main/java/org/eclipse/jetty/http/HttpURI.java | 2 +- .../java/org/eclipse/jetty/http/HttpVersion.java | 8 ++++---- .../eclipse/jetty/http/PreEncodedHttpField.java | 2 +- .../java/org/eclipse/jetty/http/HttpTester.java | 2 +- .../jetty/http2/BufferingFlowControlStrategy.java | 2 +- .../java/org/eclipse/jetty/http2/ErrorCode.java | 4 ++-- .../java/org/eclipse/jetty/http2/ISession.java | 2 +- .../main/java/org/eclipse/jetty/http2/IStream.java | 2 +- .../java/org/eclipse/jetty/http2/api/Session.java | 8 ++++---- .../java/org/eclipse/jetty/http2/api/Stream.java | 8 ++++---- .../eclipse/jetty/http2/parser/PrefaceParser.java | 2 +- .../http/HttpClientTransportOverHTTP2Test.java | 2 +- .../src/main/config/etc/jetty-http2.xml | 2 +- .../src/main/config/etc/jetty-http2c.xml | 2 +- .../server/HTTP2CServerConnectionFactory.java | 4 ++-- .../jetty/http2/server/HTTP2ServerConnection.java | 2 +- .../jetty/http2/server/HTTP2CServerTest.java | 6 +++--- .../jetty-osgi-boot/jettyhome/etc/jetty-http.xml | 4 ++-- .../etc/jetty-http-boot-context-as-service.xml | 4 ++-- .../etc/jetty-http-boot-webapp-as-service.xml | 4 ++-- .../etc/jetty-http-boot-with-annotations.xml | 4 ++-- .../etc/jetty-http-boot-with-javax-websocket.xml | 4 ++-- .../test/config/etc/jetty-http-boot-with-jsp.xml | 4 ++-- .../config/etc/jetty-http-boot-with-websocket.xml | 4 ++-- .../src/test/config/etc/jetty-http.xml | 4 ++-- .../src/test/config/etc/jetty-http2-jdk9.xml | 2 +- .../src/test/config/etc/jetty-http2.xml | 2 +- .../src/test/config/etc/jetty-https.xml | 2 +- .../jetty-rewrite.xml | 2 +- .../security/ConfigurableSpnegoLoginService.java | 2 +- .../jetty/security/ConstraintSecurityHandler.java | 2 +- .../authentication/SessionAuthentication.java | 2 +- .../org/eclipse/jetty/security/ConstraintTest.java | 4 ++-- jetty-server/src/main/config/etc/jetty-http.xml | 4 ++-- jetty-server/src/main/config/etc/jetty-https.xml | 2 +- jetty-server/src/main/config/modules/http.mod | 2 +- .../eclipse/jetty/server/AbstractConnector.java | 2 +- .../eclipse/jetty/server/ConnectionFactory.java | 6 +++--- .../java/org/eclipse/jetty/server/HttpChannel.java | 4 ++-- .../eclipse/jetty/server/HttpChannelOverHttp.java | 4 ++-- .../eclipse/jetty/server/HttpConfiguration.java | 14 +++++++------- .../jetty/server/OptionalSslConnectionFactory.java | 2 +- .../jetty/server/ResourceContentFactory.java | 2 +- .../java/org/eclipse/jetty/server/Response.java | 2 +- .../jetty/server/ServletRequestHttpWrapper.java | 2 +- .../jetty/server/ServletResponseHttpWrapper.java | 2 +- .../eclipse/jetty/server/handler/ErrorHandler.java | 4 ++-- .../jetty/server/handler/ShutdownHandler.java | 4 ++-- .../handler/gzip/GzipHttpInputInterceptor.java | 2 +- .../org/eclipse/jetty/server/session/Session.java | 2 +- .../jetty/server/session/SessionHandler.java | 2 +- .../eclipse/jetty/server/ThreadStarvationTest.java | 2 +- .../org/eclipse/jetty/servlet/RequestURITest.java | 2 +- .../java/org/eclipse/jetty/servlets/DoSFilter.java | 2 +- .../src/main/config/modules/unixsocket-http.mod | 2 +- .../src/main/config/modules/unixsocket-http2c.mod | 2 +- .../org/eclipse/jetty/util/resource/Resource.java | 2 +- .../eclipse/jetty/util/resource/URLResource.java | 2 +- .../eclipse/jetty/websocket/api/util/WSURI.java | 2 +- .../websocket/server/WebSocketUpgradeFilter.java | 2 +- .../jetty/websocket/server/TooFastClientTest.java | 2 +- .../tests/distribution/DistributionTester.java | 2 +- 79 files changed, 119 insertions(+), 119 deletions(-) diff --git a/jetty-annotations/src/test/java/org/eclipse/jetty/annotations/TestSecurityAnnotationConversions.java b/jetty-annotations/src/test/java/org/eclipse/jetty/annotations/TestSecurityAnnotationConversions.java index b3975b1c4cb..3ff372e22ee 100644 --- a/jetty-annotations/src/test/java/org/eclipse/jetty/annotations/TestSecurityAnnotationConversions.java +++ b/jetty-annotations/src/test/java/org/eclipse/jetty/annotations/TestSecurityAnnotationConversions.java @@ -173,7 +173,7 @@ public class TestSecurityAnnotationConversions public void testMethodAnnotation() throws Exception { //ServletSecurity annotation with HttpConstraint of TransportGuarantee.CONFIDENTIAL, and a list of rolesAllowed, and - //a HttpMethodConstraint for GET method that permits all and has TransportGuarantee.NONE (ie is default) + //an HttpMethodConstraint for GET method that permits all and has TransportGuarantee.NONE (ie is default) WebAppContext wac = makeWebAppContext(Method1Servlet.class.getCanonicalName(), "method1Servlet", new String[]{ "/foo/*", "*.foo" diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClientTransport.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClientTransport.java index b75469f86cf..35b12c0c3f1 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClientTransport.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClientTransport.java @@ -28,7 +28,7 @@ import org.eclipse.jetty.io.ClientConnectionFactory; * in order to plug-in a different transport for {@link HttpClient}. * <p> * While the {@link HttpClient} APIs define the HTTP semantic (request, response, headers, etc.) - * <em>how</em> a HTTP exchange is carried over the network depends on implementations of this class. + * <em>how</em> an HTTP exchange is carried over the network depends on implementations of this class. * <p> * The default implementation uses the HTTP protocol to carry over the network the HTTP exchange, * but the HTTP exchange may also be carried using the FCGI protocol, the HTTP/2 protocol or, diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpContent.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpContent.java index 4487033e5f6..17c8d3ab1bd 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpContent.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpContent.java @@ -32,7 +32,7 @@ import org.eclipse.jetty.util.log.Logger; /** * {@link HttpContent} is a stateful, linear representation of the request content provided * by a {@link ContentProvider} that can be traversed one-way to obtain content buffers to - * send to a HTTP server. + * send to an HTTP server. * <p> * {@link HttpContent} offers the notion of a one-way cursor to traverse the content. * The cursor starts in a virtual "before" position and can be advanced using {@link #advance()} diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java index 1fba1fdf247..991768f1121 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java @@ -51,7 +51,7 @@ import org.eclipse.jetty.util.log.Logger; * <ol> * <li>{@link #responseBegin(HttpExchange)}, when the HTTP response data containing the HTTP status code * is available</li> - * <li>{@link #responseHeader(HttpExchange, HttpField)}, when a HTTP field is available</li> + * <li>{@link #responseHeader(HttpExchange, HttpField)}, when an HTTP field is available</li> * <li>{@link #responseHeaders(HttpExchange)}, when all HTTP headers are available</li> * <li>{@link #responseContent(HttpExchange, ByteBuffer, Callback)}, when HTTP content is available</li> * <li>{@link #responseSuccess(HttpExchange)}, when the response is successful</li> diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRedirector.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRedirector.java index 4e6a6a0799c..565a092f64d 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRedirector.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRedirector.java @@ -82,7 +82,7 @@ public class HttpRedirector /** * @param response the response to check for redirects - * @return whether the response code is a HTTP redirect code + * @return whether the response code is an HTTP redirect code */ public boolean isRedirect(Response response) { diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java index dd26cbe8335..0ede8794655 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java @@ -869,7 +869,7 @@ public class HttpRequest implements Request } catch (URISyntaxException x) { - // The "path" of a HTTP request may not be a URI, + // The "path" of an HTTP request may not be a URI, // for example for CONNECT 127.0.0.1:8080. return null; } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java index a13c056595f..0442940df5e 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java @@ -40,7 +40,7 @@ import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.util.Fields; /** - * <p>{@link Request} represents a HTTP request, and offers a fluent interface to customize + * <p>{@link Request} represents an HTTP request, and offers a fluent interface to customize * various attributes such as the path, the headers, the content, etc.</p> * <p>You can create {@link Request} objects via {@link HttpClient#newRequest(String)} and * you can send them using either {@link #send()} for a blocking semantic, or diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Response.java b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Response.java index a5874fbcad1..99d37164ddf 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Response.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Response.java @@ -29,7 +29,7 @@ import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.util.Callback; /** - * <p>{@link Response} represents a HTTP response and offers methods to retrieve status code, HTTP version + * <p>{@link Response} represents an HTTP response and offers methods to retrieve status code, HTTP version * and headers.</p> * <p>{@link Response} objects are passed as parameters to {@link Response.Listener} callbacks, or as * future result of {@link Request#send()}.</p> diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java index 1666a60278e..15e239eee06 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java @@ -161,7 +161,7 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res } /** - * Parses a HTTP response in the receivers buffer. + * Parses an HTTP response in the receivers buffer. * * @return true to indicate that parsing should be interrupted (and will be resumed by another thread). */ diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java index 9d22bd0ee9e..3c4b362a324 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java @@ -494,7 +494,7 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest .scheme(scenario.getScheme()) .onResponseBegin(response1 -> { - // Simulate a HTTP 1.0 response has been received. + // Simulate an HTTP 1.0 response has been received. ((HttpResponse)response1).version(HttpVersion.HTTP_1_0); }) .send(); diff --git a/jetty-continuation/src/main/java/org/eclipse/jetty/continuation/Continuation.java b/jetty-continuation/src/main/java/org/eclipse/jetty/continuation/Continuation.java index c0104dbeecb..c5edaf09993 100644 --- a/jetty-continuation/src/main/java/org/eclipse/jetty/continuation/Continuation.java +++ b/jetty-continuation/src/main/java/org/eclipse/jetty/continuation/Continuation.java @@ -26,7 +26,7 @@ import javax.servlet.ServletResponseWrapper; /** * Continuation. * <p> - * A continuation is a mechanism by which a HTTP Request can be suspended and + * A continuation is a mechanism by which an HTTP Request can be suspended and * restarted after a timeout or an asynchronous event has occurred. * <p> * The continuation mechanism is a portable mechanism that will work diff --git a/jetty-deploy/src/test/resources/jetty-http.xml b/jetty-deploy/src/test/resources/jetty-http.xml index 57a4fb5567b..43ed57d5425 100644 --- a/jetty-deploy/src/test/resources/jetty-http.xml +++ b/jetty-deploy/src/test/resources/jetty-http.xml @@ -1,10 +1,10 @@ <?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"> -<!-- ============================================================= --><!-- Configure the Jetty Server instance with an ID "Server" --><!-- by adding a HTTP connector. --><!-- This configuration must be used in conjunction with jetty.xml --><!-- ============================================================= --> +<!-- ============================================================= --><!-- Configure the Jetty Server instance with an ID "Server" --><!-- by adding an HTTP connector. --><!-- This configuration must be used in conjunction with jetty.xml --><!-- ============================================================= --> <Configure id="Server" class="org.eclipse.jetty.server.Server"> <!-- =========================================================== --> - <!-- Add a HTTP Connector. --> + <!-- Add an HTTP Connector. --> <!-- Configure an o.e.j.server.ServerConnector with a single --> <!-- HttpConnectionFactory instance using the common httpConfig --> <!-- instance defined in jetty.xml --> diff --git a/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/proxy/FastCGIProxyServlet.java b/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/proxy/FastCGIProxyServlet.java index 6b847cac9aa..42f82635caf 100644 --- a/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/proxy/FastCGIProxyServlet.java +++ b/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/proxy/FastCGIProxyServlet.java @@ -44,7 +44,7 @@ import org.eclipse.jetty.util.ProcessorUtils; /** * Specific implementation of {@link org.eclipse.jetty.proxy.AsyncProxyServlet.Transparent} for FastCGI. * <p> - * This servlet accepts a HTTP request and transforms it into a FastCGI request + * This servlet accepts an HTTP request and transforms it into a FastCGI request * that is sent to the FastCGI server specified in the {@code proxyTo} * init-param. * <p> 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 2379bcef99e..4b6128182fc 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 @@ -21,7 +21,7 @@ package org.eclipse.jetty.http; import org.eclipse.jetty.util.HostPort; /** - * A HttpField holding a preparsed Host and port number + * An HttpField holding a preparsed Host and port number * * @see HostPort */ diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpField.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpField.java index 5f12147772f..db7bce9072b 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpField.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpField.java @@ -23,7 +23,7 @@ import java.util.Objects; import org.eclipse.jetty.util.StringUtil; /** - * A HTTP Field + * An HTTP Field */ public class HttpField { diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpMethod.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpMethod.java index 741be954df5..016f3dcbbee 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpMethod.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpMethod.java @@ -48,7 +48,7 @@ public enum HttpMethod * @param bytes Array containing ISO-8859-1 characters * @param position The first valid index * @param limit The first non valid index - * @return A HttpMethod if a match or null if no easy match. + * @return An HttpMethod if a match or null if no easy match. */ public static HttpMethod lookAheadGet(byte[] bytes, final int position, int limit) { @@ -110,7 +110,7 @@ public enum HttpMethod * Optimized lookup to find a method name and trailing space in a byte array. * * @param buffer buffer containing ISO-8859-1 characters, it is not modified. - * @return A HttpMethod if a match or null if no easy match. + * @return An HttpMethod if a match or null if no easy match. */ public static HttpMethod lookAheadGet(ByteBuffer buffer) { diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java index 7ebd682f667..435579c9df0 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java @@ -477,7 +477,7 @@ public class HttpParser return t; } - /* Quick lookahead for the start state looking for a request method or a HTTP version, + /* Quick lookahead for the start state looking for a request method or an HTTP version, * otherwise skip white space until something else to parse. */ private boolean quickStart(ByteBuffer buffer) @@ -1873,14 +1873,14 @@ public class HttpParser boolean messageComplete(); /** - * This is the method called by parser when a HTTP Header name and value is found + * This is the method called by parser when an HTTP Header name and value is found * * @param field The field parsed */ void parsedHeader(HttpField field); /** - * This is the method called by parser when a HTTP Trailer name and value is found + * This is the method called by parser when an HTTP Trailer name and value is found * * @param field The field parsed */ @@ -1890,7 +1890,7 @@ public class HttpParser /** * Called to signal that an EOF was received unexpectedly - * during the parsing of a HTTP message + * during the parsing of an HTTP message */ void earlyEOF(); diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java index 5f5fec2636a..4fb2a36989e 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java @@ -31,7 +31,7 @@ import org.eclipse.jetty.util.UrlEncoded; /** * Http URI. - * Parse a HTTP URI from a string or byte array. Given a URI + * Parse an HTTP URI from a string or byte array. Given a URI * <code>http://user@host:port/path/info;param?query#fragment</code> * this class will split it into the following undecoded optional elements:<ul> * <li>{@link #getScheme()} - http:</li> diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpVersion.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpVersion.java index 762372ef5cc..12991ad105f 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpVersion.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpVersion.java @@ -42,12 +42,12 @@ public enum HttpVersion } /** - * Optimised lookup to find a Http Version and whitespace in a byte array. + * Optimised lookup to find an Http Version and whitespace in a byte array. * * @param bytes Array containing ISO-8859-1 characters * @param position The first valid index * @param limit The first non valid index - * @return A HttpMethod if a match or null if no easy match. + * @return An HttpMethod if a match or null if no easy match. */ public static HttpVersion lookAheadGet(byte[] bytes, int position, int limit) { @@ -84,10 +84,10 @@ public enum HttpVersion } /** - * Optimised lookup to find a HTTP Version and trailing white space in a byte array. + * Optimised lookup to find an HTTP Version and trailing white space in a byte array. * * @param buffer buffer containing ISO-8859-1 characters - * @return A HttpVersion if a match or null if no easy match. + * @return An HttpVersion if a match or null if no easy match. */ public static HttpVersion lookAheadGet(ByteBuffer buffer) { diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/PreEncodedHttpField.java b/jetty-http/src/main/java/org/eclipse/jetty/http/PreEncodedHttpField.java index 35ec9a7cd40..cf9cf1c0752 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/PreEncodedHttpField.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/PreEncodedHttpField.java @@ -29,7 +29,7 @@ import org.eclipse.jetty.util.log.Logger; /** * Pre encoded HttpField. - * <p>A HttpField that will be cached and used many times can be created as + * <p>An HttpField that will be cached and used many times can be created as * a {@link PreEncodedHttpField}, which will use the {@link HttpFieldPreEncoder} * instances discovered by the {@link ServiceLoader} to pre-encode the header * for each version of HTTP in use. This will save garbage diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpTester.java b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpTester.java index 27d494e3649..eee24e824be 100644 --- a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpTester.java +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpTester.java @@ -32,7 +32,7 @@ import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; /** - * A HTTP Testing helper class. + * An HTTP Testing helper class. * * Example usage: * <pre> diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/BufferingFlowControlStrategy.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/BufferingFlowControlStrategy.java index a5a57fa2666..7eff31981e2 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/BufferingFlowControlStrategy.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/BufferingFlowControlStrategy.java @@ -170,7 +170,7 @@ public class BufferingFlowControlStrategy extends AbstractFlowControlStrategy // and here we keep track of its max value. // Updating the max session recv window is done here - // so that if a peer decides to send an unilateral + // so that if a peer decides to send a unilateral // window update to enlarge the session window, // without the corresponding data consumption, here // we can track it. diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/ErrorCode.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/ErrorCode.java index ac36211f847..4269bc67d1f 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/ErrorCode.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/ErrorCode.java @@ -40,7 +40,7 @@ public enum ErrorCode */ INTERNAL_ERROR(2), /** - * Indicates a HTTP/2 flow control violation. + * Indicates an HTTP/2 flow control violation. */ FLOW_CONTROL_ERROR(3), /** @@ -68,7 +68,7 @@ public enum ErrorCode */ COMPRESSION_ERROR(9), /** - * Indicates that the connection established by a HTTP CONNECT was abnormally closed. + * Indicates that the connection established by an HTTP CONNECT was abnormally closed. */ HTTP_CONNECT_ERROR(10), /** diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/ISession.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/ISession.java index 2e198fa5931..3df57d39b29 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/ISession.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/ISession.java @@ -30,7 +30,7 @@ import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Promise; /** - * <p>The SPI interface for implementing a HTTP/2 session.</p> + * <p>The SPI interface for implementing an HTTP/2 session.</p> * <p>This class extends {@link Session} by adding the methods required to * implement the HTTP/2 session functionalities.</p> */ diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/IStream.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/IStream.java index c85e25b287f..144ed9190c6 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/IStream.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/IStream.java @@ -25,7 +25,7 @@ import org.eclipse.jetty.http2.frames.Frame; import org.eclipse.jetty.util.Callback; /** - * <p>The SPI interface for implementing a HTTP/2 stream.</p> + * <p>The SPI interface for implementing an HTTP/2 stream.</p> * <p>This class extends {@link Stream} by adding the methods required to * implement the HTTP/2 stream functionalities.</p> */ diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Session.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Session.java index 7c5472bd135..2ba0b3d78ed 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Session.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Session.java @@ -32,7 +32,7 @@ import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Promise; /** - * <p>A {@link Session} represents the client-side endpoint of a HTTP/2 connection to a single origin server.</p> + * <p>A {@link Session} represents the client-side endpoint of an HTTP/2 connection to a single origin server.</p> * <p>Once a {@link Session} has been obtained, it can be used to open HTTP/2 streams:</p> * <pre> * Session session = ...; @@ -127,7 +127,7 @@ public interface Session /** * <p>A {@link Listener} is the passive counterpart of a {@link Session} and - * receives events happening on a HTTP/2 connection.</p> + * receives events happening on an HTTP/2 connection.</p> * * @see Session */ @@ -151,9 +151,9 @@ public interface Session /** * <p>Callback method invoked when a new stream is being created upon - * receiving a HEADERS frame representing a HTTP request.</p> + * receiving a HEADERS frame representing an HTTP request.</p> * <p>Applications should implement this method to process HTTP requests, - * typically providing a HTTP response via + * typically providing an HTTP response via * {@link Stream#headers(HeadersFrame, Callback)}.</p> * <p>Applications can detect whether request DATA frames will be arriving * by testing {@link HeadersFrame#isEndStream()}. If the application is diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Stream.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Stream.java index 681c0870ff1..bb413d8d953 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Stream.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Stream.java @@ -29,8 +29,8 @@ import org.eclipse.jetty.util.Promise; * <p>A {@link Stream} represents a bidirectional exchange of data on top of a {@link Session}.</p> * <p>Differently from socket streams, where the input and output streams are permanently associated * with the socket (and hence with the connection that the socket represents), there can be multiple - * HTTP/2 streams present concurrent for a HTTP/2 session.</p> - * <p>A {@link Stream} maps to a HTTP request/response cycle, and after the request/response cycle is + * HTTP/2 streams present concurrent for an HTTP/2 session.</p> + * <p>A {@link Stream} maps to an HTTP request/response cycle, and after the request/response cycle is * completed, the stream is closed and removed from the session.</p> * <p>Like {@link Session}, {@link Stream} is the active part and by calling its API applications * can generate events on the stream; conversely, {@link Stream.Listener} is the passive part, and @@ -51,7 +51,7 @@ public interface Stream Session getSession(); /** - * <p>Sends the given HEADERS {@code frame} representing a HTTP response.</p> + * <p>Sends the given HEADERS {@code frame} representing an HTTP response.</p> * * @param frame the HEADERS frame to send * @param callback the callback that gets notified when the frame has been sent @@ -131,7 +131,7 @@ public interface Stream /** * <p>A {@link Stream.Listener} is the passive counterpart of a {@link Stream} and receives - * events happening on a HTTP/2 stream.</p> + * events happening on an HTTP/2 stream.</p> * * @see Stream */ diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PrefaceParser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PrefaceParser.java index 3a6058878a4..741bba433a7 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PrefaceParser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PrefaceParser.java @@ -42,7 +42,7 @@ public class PrefaceParser * <p>Advances this parser after the {@link PrefaceFrame#PREFACE_PREAMBLE_BYTES}.</p> * <p>This allows the HTTP/1.1 parser to parse the preamble of the preface, * which is a legal HTTP/1.1 request, and this parser will parse the remaining - * bytes, that are not parseable by a HTTP/1.1 parser.</p> + * bytes, that are not parseable by an HTTP/1.1 parser.</p> */ protected void directUpgrade() { diff --git a/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2Test.java b/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2Test.java index 2e9e2cf725a..6e782a738b0 100644 --- a/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2Test.java +++ b/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2Test.java @@ -199,7 +199,7 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest .onRequestBegin(request -> { if (request.getVersion() != HttpVersion.HTTP_2) - request.abort(new Exception("Not a HTTP/2 request")); + request.abort(new Exception("Not an HTTP/2 request")); }) .send(); diff --git a/jetty-http2/http2-server/src/main/config/etc/jetty-http2.xml b/jetty-http2/http2-server/src/main/config/etc/jetty-http2.xml index 6ced1d68a20..56777719fdf 100644 --- a/jetty-http2/http2-server/src/main/config/etc/jetty-http2.xml +++ b/jetty-http2/http2-server/src/main/config/etc/jetty-http2.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"> -<!-- ============================================================= --><!-- Configure a HTTP2 on the ssl connector. --><!-- ============================================================= --> +<!-- ============================================================= --><!-- Configure an HTTP2 on the ssl connector. --><!-- ============================================================= --> <Configure id="sslConnector" class="org.eclipse.jetty.server.ServerConnector"> <Call name="addConnectionFactory"> <Arg> diff --git a/jetty-http2/http2-server/src/main/config/etc/jetty-http2c.xml b/jetty-http2/http2-server/src/main/config/etc/jetty-http2c.xml index b4cae9a549c..37cb745fe0e 100644 --- a/jetty-http2/http2-server/src/main/config/etc/jetty-http2c.xml +++ b/jetty-http2/http2-server/src/main/config/etc/jetty-http2c.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"> -<!-- ============================================================= --><!-- Configure a HTTP2 on the ssl connector. --><!-- ============================================================= --> +<!-- ============================================================= --><!-- Configure an HTTP2 on the ssl connector. --><!-- ============================================================= --> <Configure id="httpConnector" class="org.eclipse.jetty.server.ServerConnector"> <Call name="addConnectionFactory"> <Arg> diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2CServerConnectionFactory.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2CServerConnectionFactory.java index cc0b9925311..4ed23cf2fea 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2CServerConnectionFactory.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2CServerConnectionFactory.java @@ -39,8 +39,8 @@ import org.eclipse.jetty.util.log.Logger; * </p> * <p>If used in combination with a {@link HttpConnectionFactory} as the * default protocol, this factory can support the non-standard direct - * update mechanism, where a HTTP1 request of the form "PRI * HTTP/2.0" - * is used to trigger a switch to a HTTP2 connection. This approach + * update mechanism, where an HTTP1 request of the form "PRI * HTTP/2.0" + * is used to trigger a switch to an HTTP2 connection. This approach * allows a single port to accept either HTTP/1 or HTTP/2 direct * connections. */ diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java index 331696417d2..f8dfdbf7d36 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java @@ -64,7 +64,7 @@ import org.eclipse.jetty.util.TypeUtil; public class HTTP2ServerConnection extends HTTP2Connection implements Connection.UpgradeTo { /** - * @param protocol A HTTP2 protocol variant + * @param protocol An HTTP2 protocol variant * @return True if the protocol version is supported */ public static boolean isSupportedProtocol(String protocol) diff --git a/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/HTTP2CServerTest.java b/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/HTTP2CServerTest.java index ed1fd6e948a..48552abebac 100644 --- a/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/HTTP2CServerTest.java +++ b/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/HTTP2CServerTest.java @@ -188,7 +188,7 @@ public class HTTP2CServerTest extends AbstractServerTest assertThat(content, containsString("Hello from Jetty using HTTP/1.1")); assertThat(content, containsString("uri=/one")); - // Send a HTTP/2 request. + // Send an HTTP/2 request. headersRef.set(null); dataRef.set(null); latchRef.set(new CountDownLatch(2)); @@ -319,7 +319,7 @@ public class HTTP2CServerTest extends AbstractServerTest connector.setDefaultProtocol(connectionFactory.getProtocol()); connector.start(); - // Now send a HTTP/2 direct request, which + // Now send an HTTP/2 direct request, which // will have the PRI * HTTP/2.0 preface. byteBufferPool = new MappedByteBufferPool(); @@ -336,7 +336,7 @@ public class HTTP2CServerTest extends AbstractServerTest output.write(BufferUtil.toArray(buffer)); } - // We sent a HTTP/2 preface, but the server has no "h2c" connection + // We sent an HTTP/2 preface, but the server has no "h2c" connection // factory so it does not know how to handle this request. InputStream input = client.getInputStream(); diff --git a/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty-http.xml b/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty-http.xml index 4e571ac750a..412e644a2e2 100644 --- a/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty-http.xml +++ b/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty-http.xml @@ -1,10 +1,10 @@ <?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"> -<!-- ============================================================= --><!-- Configure the Jetty Server instance with an ID "Server" --><!-- by adding a HTTP connector. --><!-- This configuration must be used in conjunction with jetty.xml --><!-- ============================================================= --> +<!-- ============================================================= --><!-- Configure the Jetty Server instance with an ID "Server" --><!-- by adding an HTTP connector. --><!-- This configuration must be used in conjunction with jetty.xml --><!-- ============================================================= --> <Configure id="Server" class="org.eclipse.jetty.server.Server"> <!-- =========================================================== --> - <!-- Add a HTTP Connector. --> + <!-- Add an HTTP Connector. --> <!-- Configure an o.e.j.server.ServerConnector with a single --> <!-- HttpConnectionFactory instance using the common httpConfig --> <!-- instance defined in jetty.xml --> diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-context-as-service.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-context-as-service.xml index 1c820d39bbd..8e2360b85ca 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-context-as-service.xml +++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-context-as-service.xml @@ -1,10 +1,10 @@ <?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"> -<!-- ============================================================= --><!-- Configure the Jetty Server instance with an ID "Server" --><!-- by adding a HTTP connector. --><!-- This configuration must be used in conjunction with jetty.xml --><!-- ============================================================= --> +<!-- ============================================================= --><!-- Configure the Jetty Server instance with an ID "Server" --><!-- by adding an HTTP connector. --><!-- This configuration must be used in conjunction with jetty.xml --><!-- ============================================================= --> <Configure id="Server" class="org.eclipse.jetty.server.Server"> <!-- =========================================================== --> - <!-- Add a HTTP Connector. --> + <!-- Add an HTTP Connector. --> <!-- Configure an o.e.j.server.ServerConnector with a single --> <!-- HttpConnectionFactory instance using the common httpConfig --> <!-- instance defined in jetty.xml --> diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-webapp-as-service.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-webapp-as-service.xml index b56a9762469..bccf90e1204 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-webapp-as-service.xml +++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-webapp-as-service.xml @@ -1,10 +1,10 @@ <?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"> -<!-- ============================================================= --><!-- Configure the Jetty Server instance with an ID "Server" --><!-- by adding a HTTP connector. --><!-- This configuration must be used in conjunction with jetty.xml --><!-- ============================================================= --> +<!-- ============================================================= --><!-- Configure the Jetty Server instance with an ID "Server" --><!-- by adding an HTTP connector. --><!-- This configuration must be used in conjunction with jetty.xml --><!-- ============================================================= --> <Configure id="Server" class="org.eclipse.jetty.server.Server"> <!-- =========================================================== --> - <!-- Add a HTTP Connector. --> + <!-- Add an HTTP Connector. --> <!-- Configure an o.e.j.server.ServerConnector with a single --> <!-- HttpConnectionFactory instance using the common httpConfig --> <!-- instance defined in jetty.xml --> diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-annotations.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-annotations.xml index 2e79a92cc29..eab6f8936c8 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-annotations.xml +++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-annotations.xml @@ -1,10 +1,10 @@ <?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"> -<!-- ============================================================= --><!-- Configure the Jetty Server instance with an ID "Server" --><!-- by adding a HTTP connector. --><!-- This configuration must be used in conjunction with jetty.xml --><!-- ============================================================= --> +<!-- ============================================================= --><!-- Configure the Jetty Server instance with an ID "Server" --><!-- by adding an HTTP connector. --><!-- This configuration must be used in conjunction with jetty.xml --><!-- ============================================================= --> <Configure id="Server" class="org.eclipse.jetty.server.Server"> <!-- =========================================================== --> - <!-- Add a HTTP Connector. --> + <!-- Add an HTTP Connector. --> <!-- Configure an o.e.j.server.ServerConnector with a single --> <!-- HttpConnectionFactory instance using the common httpConfig --> <!-- instance defined in jetty.xml --> diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-javax-websocket.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-javax-websocket.xml index 3f8f402579e..dca7d650b74 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-javax-websocket.xml +++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-javax-websocket.xml @@ -1,10 +1,10 @@ <?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"> -<!-- ============================================================= --><!-- Configure the Jetty Server instance with an ID "Server" --><!-- by adding a HTTP connector. --><!-- This configuration must be used in conjunction with jetty.xml --><!-- ============================================================= --> +<!-- ============================================================= --><!-- Configure the Jetty Server instance with an ID "Server" --><!-- by adding an HTTP connector. --><!-- This configuration must be used in conjunction with jetty.xml --><!-- ============================================================= --> <Configure id="Server" class="org.eclipse.jetty.server.Server"> <!-- =========================================================== --> - <!-- Add a HTTP Connector. --> + <!-- Add an HTTP Connector. --> <!-- Configure an o.e.j.server.ServerConnector with a single --> <!-- HttpConnectionFactory instance using the common httpConfig --> <!-- instance defined in jetty.xml --> diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-jsp.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-jsp.xml index 00f8d58fd50..38b5a790496 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-jsp.xml +++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-jsp.xml @@ -1,10 +1,10 @@ <?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"> -<!-- ============================================================= --><!-- Configure the Jetty Server instance with an ID "Server" --><!-- by adding a HTTP connector. --><!-- This configuration must be used in conjunction with jetty.xml --><!-- ============================================================= --> +<!-- ============================================================= --><!-- Configure the Jetty Server instance with an ID "Server" --><!-- by adding an HTTP connector. --><!-- This configuration must be used in conjunction with jetty.xml --><!-- ============================================================= --> <Configure id="Server" class="org.eclipse.jetty.server.Server"> <!-- =========================================================== --> - <!-- Add a HTTP Connector. --> + <!-- Add an HTTP Connector. --> <!-- Configure an o.e.j.server.ServerConnector with a single --> <!-- HttpConnectionFactory instance using the common httpConfig --> <!-- instance defined in jetty.xml --> diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-websocket.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-websocket.xml index f313e299e39..e6857ba7b89 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-websocket.xml +++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-websocket.xml @@ -1,10 +1,10 @@ <?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"> -<!-- ============================================================= --><!-- Configure the Jetty Server instance with an ID "Server" --><!-- by adding a HTTP connector. --><!-- This configuration must be used in conjunction with jetty.xml --><!-- ============================================================= --> +<!-- ============================================================= --><!-- Configure the Jetty Server instance with an ID "Server" --><!-- by adding an HTTP connector. --><!-- This configuration must be used in conjunction with jetty.xml --><!-- ============================================================= --> <Configure id="Server" class="org.eclipse.jetty.server.Server"> <!-- =========================================================== --> - <!-- Add a HTTP Connector. --> + <!-- Add an HTTP Connector. --> <!-- Configure an o.e.j.server.ServerConnector with a single --> <!-- HttpConnectionFactory instance using the common httpConfig --> <!-- instance defined in jetty.xml --> diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http.xml index d16c478cf2b..11c1650548d 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http.xml +++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http.xml @@ -1,10 +1,10 @@ <?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"> -<!-- ============================================================= --><!-- Configure the Jetty Server instance with an ID "Server" --><!-- by adding a HTTP connector. --><!-- This configuration must be used in conjunction with jetty.xml --><!-- ============================================================= --> +<!-- ============================================================= --><!-- Configure the Jetty Server instance with an ID "Server" --><!-- by adding an HTTP connector. --><!-- This configuration must be used in conjunction with jetty.xml --><!-- ============================================================= --> <Configure id="Server" class="org.eclipse.jetty.server.Server"> <!-- =========================================================== --> - <!-- Add a HTTP Connector. --> + <!-- Add an HTTP Connector. --> <!-- Configure an o.e.j.server.ServerConnector with a single --> <!-- HttpConnectionFactory instance using the common httpConfig --> <!-- instance defined in jetty.xml --> diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http2-jdk9.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http2-jdk9.xml index 6ced1d68a20..56777719fdf 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http2-jdk9.xml +++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http2-jdk9.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"> -<!-- ============================================================= --><!-- Configure a HTTP2 on the ssl connector. --><!-- ============================================================= --> +<!-- ============================================================= --><!-- Configure an HTTP2 on the ssl connector. --><!-- ============================================================= --> <Configure id="sslConnector" class="org.eclipse.jetty.server.ServerConnector"> <Call name="addConnectionFactory"> <Arg> diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http2.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http2.xml index fbe3b2b4af0..5e85f018046 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http2.xml +++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http2.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"> -<!-- ============================================================= --><!-- Configure a HTTP2 on the ssl connector. --><!-- ============================================================= --> +<!-- ============================================================= --><!-- Configure an HTTP2 on the ssl connector. --><!-- ============================================================= --> <Configure id="sslConnector" class="org.eclipse.jetty.server.ServerConnector"> <Call name="addConnectionFactory"> <Arg> diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-https.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-https.xml index 8c6536074a1..30199cb8677 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-https.xml +++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-https.xml @@ -1,6 +1,6 @@ <?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"> -<!-- ============================================================= --><!-- Configure a HTTPS connector. --><!-- This configuration must be used in conjunction with jetty.xml --><!-- and jetty-ssl.xml. --><!-- ============================================================= --> +<!-- ============================================================= --><!-- Configure an HTTPS connector. --><!-- This configuration must be used in conjunction with jetty.xml --><!-- and jetty-ssl.xml. --><!-- ============================================================= --> <Configure id="sslConnector" class="org.eclipse.jetty.server.ServerConnector"> <Call name="addIfAbsentConnectionFactory"> diff --git a/jetty-rewrite/src/test/resources/org.mortbay.jetty.rewrite.handler/jetty-rewrite.xml b/jetty-rewrite/src/test/resources/org.mortbay.jetty.rewrite.handler/jetty-rewrite.xml index cf4c4e65bcd..fa6bc790c4f 100644 --- a/jetty-rewrite/src/test/resources/org.mortbay.jetty.rewrite.handler/jetty-rewrite.xml +++ b/jetty-rewrite/src/test/resources/org.mortbay.jetty.rewrite.handler/jetty-rewrite.xml @@ -40,7 +40,7 @@ </Call> <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> - <!-- To add a HTTPS SSL listener --> + <!-- To add an HTTPS SSL listener --> <!-- see jetty-ssl.xml to add an ssl connector. use --> <!-- java -jar start.jar etc/jetty-ssl.xml --> <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/ConfigurableSpnegoLoginService.java b/jetty-security/src/main/java/org/eclipse/jetty/security/ConfigurableSpnegoLoginService.java index 8b8724d642c..4b93b7e7ca6 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/ConfigurableSpnegoLoginService.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/ConfigurableSpnegoLoginService.java @@ -51,7 +51,7 @@ import org.ietf.jgss.Oid; * of the {@link #getServiceName() service name} and the {@link #getHostName() host name}, * for example {@code HTTP/wonder.com}, using a {@code keyTab} file as the service principal * credentials.</p> - * <p>Upon receiving a HTTP request, the server tries to authenticate the client + * <p>Upon receiving an HTTP request, the server tries to authenticate the client * calling {@link #login(String, Object, ServletRequest)} where the GSS APIs are used to * verify client tokens and (perhaps after a few round-trips) a {@code GSSContext} is * established.</p> diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintSecurityHandler.java b/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintSecurityHandler.java index 6a50ecd8c29..e6224c102cb 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintSecurityHandler.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintSecurityHandler.java @@ -806,7 +806,7 @@ public class ConstraintSecurityHandler extends SecurityHandler implements Constr { //an exact method name if (!hasOmissions) - //a http-method does not have http-method-omission to cover the other method names + //an http-method does not have http-method-omission to cover the other method names uncoveredPaths.add(path); } } diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SessionAuthentication.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SessionAuthentication.java index 1a672615e96..1a6de7a1874 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SessionAuthentication.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SessionAuthentication.java @@ -39,7 +39,7 @@ import org.eclipse.jetty.util.log.Logger; * * When a user has been successfully authenticated with some types * of Authenticator, the Authenticator stashes a SessionAuthentication - * into a HttpSession to remember that the user is authenticated. + * into an HttpSession to remember that the user is authenticated. */ public class SessionAuthentication extends AbstractUserAuthentication implements Serializable, HttpSessionActivationListener, HttpSessionBindingListener diff --git a/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java b/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java index 67140ff6d70..ffefdea617e 100644 --- a/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java +++ b/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java @@ -423,7 +423,7 @@ public class ConstraintTest assertEquals(1, uncoveredPaths.size()); assertThat("/user/*", is(in(uncoveredPaths))); - //Test an explicitly named method with a http-method-omission to cover all other methods + //Test an explicitly named method with an http-method-omission to cover all other methods Constraint constraint2a = new Constraint(); constraint2a.setAuthenticate(true); constraint2a.setName("forbid constraint"); @@ -437,7 +437,7 @@ public class ConstraintTest assertNotNull(uncoveredPaths); assertEquals(0, uncoveredPaths.size()); - //Test a http-method-omission only + //Test an http-method-omission only Constraint constraint3 = new Constraint(); constraint3.setAuthenticate(true); constraint3.setName("omit constraint"); diff --git a/jetty-server/src/main/config/etc/jetty-http.xml b/jetty-server/src/main/config/etc/jetty-http.xml index 13b74d5be96..e93bb68dfc1 100644 --- a/jetty-server/src/main/config/etc/jetty-http.xml +++ b/jetty-server/src/main/config/etc/jetty-http.xml @@ -1,10 +1,10 @@ <?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"> -<!-- ============================================================= --><!-- Configure the Jetty Server instance with an ID "Server" --><!-- by adding a HTTP connector. --><!-- This configuration must be used in conjunction with jetty.xml --><!-- ============================================================= --> +<!-- ============================================================= --><!-- Configure the Jetty Server instance with an ID "Server" --><!-- by adding an HTTP connector. --><!-- This configuration must be used in conjunction with jetty.xml --><!-- ============================================================= --> <Configure id="Server" class="org.eclipse.jetty.server.Server"> <!-- =========================================================== --> - <!-- Add a HTTP Connector. --> + <!-- Add an HTTP Connector. --> <!-- Configure an o.e.j.server.ServerConnector with a single --> <!-- HttpConnectionFactory instance using the common httpConfig --> <!-- instance defined in jetty.xml --> diff --git a/jetty-server/src/main/config/etc/jetty-https.xml b/jetty-server/src/main/config/etc/jetty-https.xml index 35bb3a6baaf..99abd18df59 100644 --- a/jetty-server/src/main/config/etc/jetty-https.xml +++ b/jetty-server/src/main/config/etc/jetty-https.xml @@ -1,6 +1,6 @@ <?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"> -<!-- ============================================================= --><!-- Configure a HTTPS connector. --><!-- This configuration must be used in conjunction with jetty.xml --><!-- and jetty-ssl.xml. --><!-- ============================================================= --> +<!-- ============================================================= --><!-- Configure an HTTPS connector. --><!-- This configuration must be used in conjunction with jetty.xml --><!-- and jetty-ssl.xml. --><!-- ============================================================= --> <Configure id="sslConnector" class="org.eclipse.jetty.server.ServerConnector"> <Call name="addIfAbsentConnectionFactory"> diff --git a/jetty-server/src/main/config/modules/http.mod b/jetty-server/src/main/config/modules/http.mod index 430d8519e2e..7c20880043f 100644 --- a/jetty-server/src/main/config/modules/http.mod +++ b/jetty-server/src/main/config/modules/http.mod @@ -1,7 +1,7 @@ DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html [description] -Enables a HTTP connector on the server. +Enables an HTTP connector on the server. By default HTTP/1 is support, but HTTP2C can be added to the connector with the http2c module. diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java index f1425dde301..1723bcc73df 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java @@ -116,7 +116,7 @@ import org.eclipse.jetty.util.thread.ThreadPoolBudget; * {@link ConnectionFactory}s may also create temporary {@link org.eclipse.jetty.io.Connection} instances that will exchange bytes * over the connection to determine what is the next protocol to use. For example the ALPN protocol is an extension * of SSL to allow a protocol to be specified during the SSL handshake. ALPN is used by the HTTP/2 protocol to - * negotiate the protocol that the client and server will speak. Thus to accept a HTTP/2 connection, the + * negotiate the protocol that the client and server will speak. Thus to accept an HTTP/2 connection, the * connector will be configured with {@link ConnectionFactory}s for "SSL-ALPN", "h2", "http/1.1" * with the default protocol being "SSL-ALPN". Thus a newly accepted connection uses "SSL-ALPN", which specifies a * SSLConnectionFactory with "ALPN" as the next protocol. Thus an SSL connection instance is created chained to an ALPN diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectionFactory.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectionFactory.java index 9a24133b1a5..377947ca7fd 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectionFactory.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectionFactory.java @@ -35,11 +35,11 @@ import org.eclipse.jetty.io.EndPoint; * A ConnectionFactory has a protocol name that represents the protocol of the Connections * created. Example of protocol names include: * <dl> - * <dt>http</dt><dd>Creates a HTTP connection that can handle multiple versions of HTTP from 0.9 to 1.1</dd> - * <dt>h2</dt><dd>Creates a HTTP/2 connection that handles the HTTP/2 protocol</dd> + * <dt>http</dt><dd>Creates an HTTP connection that can handle multiple versions of HTTP from 0.9 to 1.1</dd> + * <dt>h2</dt><dd>Creates an HTTP/2 connection that handles the HTTP/2 protocol</dd> * <dt>SSL-XYZ</dt><dd>Create an SSL connection chained to a connection obtained from a connection factory * with a protocol "XYZ".</dd> - * <dt>SSL-http</dt><dd>Create an SSL connection chained to a HTTP connection (aka https)</dd> + * <dt>SSL-http</dt><dd>Create an SSL connection chained to an HTTP connection (aka https)</dd> * <dt>SSL-ALPN</dt><dd>Create an SSL connection chained to a ALPN connection, that uses a negotiation with * the client to determine the next protocol.</dd> * </dl> diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index 38760aadac4..e48d6f22156 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -61,7 +61,7 @@ import org.eclipse.jetty.util.thread.Scheduler; /** * HttpChannel represents a single endpoint for HTTP semantic processing. - * The HttpChannel is both a HttpParser.RequestHandler, where it passively receives events from + * The HttpChannel is both an HttpParser.RequestHandler, where it passively receives events from * an incoming HTTP request, and a Runnable, where it actively takes control of the request/response * life cycle and calls the application (perhaps suspending and resuming with multiple calls to run). * The HttpChannel signals the switch from passive mode to active mode by returning true to one of the @@ -1016,7 +1016,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor /** * <p>Listener for {@link HttpChannel} events.</p> * <p>HttpChannel will emit events for the various phases it goes through while - * processing a HTTP request and response.</p> + * processing an HTTP request and response.</p> * <p>Implementations of this interface may listen to those events to track * timing and/or other values such as request URI, etc.</p> * <p>The events parameters, especially the {@link Request} object, may be diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelOverHttp.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelOverHttp.java index b4e88f029ee..3d12c04f1eb 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelOverHttp.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelOverHttp.java @@ -44,7 +44,7 @@ import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; /** - * A HttpChannel customized to be transported over the HTTP/1 protocol + * An HttpChannel customized to be transported over the HTTP/1 protocol */ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.RequestHandler, HttpParser.ComplianceHandler { @@ -411,7 +411,7 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque } /** - * <p>Attempts to perform a HTTP/1.1 upgrade.</p> + * <p>Attempts to perform an HTTP/1.1 upgrade.</p> * <p>The upgrade looks up a {@link ConnectionFactory.Upgrading} from the connector * matching the protocol specified in the {@code Upgrade} header.</p> * <p>The upgrade may succeed, be ignored (which can allow a later handler to implement) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java index 9aa9b62fcc8..e5bd93fde16 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java @@ -39,7 +39,7 @@ import org.eclipse.jetty.util.log.Logger; /** * HTTP Configuration. * <p>This class is a holder of HTTP configuration for use by the - * {@link HttpChannel} class. Typically a HTTPConfiguration instance + * {@link HttpChannel} class. Typically an HTTPConfiguration instance * is instantiated and passed to a {@link ConnectionFactory} that can * create HTTP channels (e.g. HTTP, AJP or FCGI).</p> * <p>The configuration held by this class is not for the wire protocol, @@ -184,19 +184,19 @@ public class HttpConfiguration implements Dumpable return _outputAggregationSize; } - @ManagedAttribute("The maximum allowed size in bytes for a HTTP request header") + @ManagedAttribute("The maximum allowed size in bytes for an HTTP request header") public int getRequestHeaderSize() { return _requestHeaderSize; } - @ManagedAttribute("The maximum allowed size in bytes for a HTTP response header") + @ManagedAttribute("The maximum allowed size in bytes for an HTTP response header") public int getResponseHeaderSize() { return _responseHeaderSize; } - @ManagedAttribute("The maximum allowed size in bytes for a HTTP header field cache") + @ManagedAttribute("The maximum allowed size in bytes for an HTTP header field cache") public int getHeaderCacheSize() { return _headerCacheSize; @@ -221,20 +221,20 @@ public class HttpConfiguration implements Dumpable } /** - * <p>The max idle time is applied to a HTTP request for IO operations and + * <p>The max idle time is applied to an HTTP request for IO operations and * delayed dispatch.</p> * * @return the max idle time in ms or if == 0 implies an infinite timeout, <0 * implies no HTTP channel timeout and the connection timeout is used instead. */ - @ManagedAttribute("The idle timeout in ms for I/O operations during the handling of a HTTP request") + @ManagedAttribute("The idle timeout in ms for I/O operations during the handling of an HTTP request") public long getIdleTimeout() { return _idleTimeout; } /** - * <p>The max idle time is applied to a HTTP request for IO operations and + * <p>The max idle time is applied to an HTTP request for IO operations and * delayed dispatch.</p> * * @param timeoutMs the max idle time in ms or if == 0 implies an infinite timeout, <0 diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/OptionalSslConnectionFactory.java b/jetty-server/src/main/java/org/eclipse/jetty/server/OptionalSslConnectionFactory.java index 71b40c0fa82..41118eb081d 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/OptionalSslConnectionFactory.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/OptionalSslConnectionFactory.java @@ -95,7 +95,7 @@ public class OptionalSslConnectionFactory extends AbstractConnectionFactory int byte2 = buffer.get(1) & 0xFF; if (byte1 == 'G' && byte2 == 'E') { - // Plain text HTTP to a HTTPS port, + // Plain text HTTP to an HTTPS port, // write a minimal response. String body = "<!DOCTYPE html>\r\n" + diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceContentFactory.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceContentFactory.java index 95a9d83e533..b6d58845de1 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceContentFactory.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceContentFactory.java @@ -32,7 +32,7 @@ import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceFactory; /** - * A HttpContent.Factory for transient content (not cached). The HttpContent's created by + * An HttpContent.Factory for transient content (not cached). The HttpContent's created by * this factory are not intended to be cached, so memory limits for individual * HttpOutput streams are enforced. */ diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index 04870c50949..1ef97d7286e 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -410,7 +410,7 @@ public class Response implements HttpServletResponse /** * Sends a 102-Processing response. - * If the connection is a HTTP connection, the version is 1.1 and the + * If the connection is an HTTP connection, the version is 1.1 and the * request has a Expect header starting with 102, then a 102 response is * sent. This indicates that the request still be processed and real response * can still be sent. This method is called by sendError if it is passed 102. diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ServletRequestHttpWrapper.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ServletRequestHttpWrapper.java index c754a656694..87c6b753317 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/ServletRequestHttpWrapper.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ServletRequestHttpWrapper.java @@ -35,7 +35,7 @@ import javax.servlet.http.Part; /** * ServletRequestHttpWrapper * - * Class to tunnel a ServletRequest via a HttpServletRequest + * Class to tunnel a ServletRequest via an HttpServletRequest */ public class ServletRequestHttpWrapper extends ServletRequestWrapper implements HttpServletRequest { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ServletResponseHttpWrapper.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ServletResponseHttpWrapper.java index e3499962918..ff01136cc9c 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/ServletResponseHttpWrapper.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ServletResponseHttpWrapper.java @@ -28,7 +28,7 @@ import javax.servlet.http.HttpServletResponse; /** * ServletResponseHttpWrapper * - * Wrapper to tunnel a ServletResponse via a HttpServletResponse + * Wrapper to tunnel a ServletResponse via an HttpServletResponse */ public class ServletResponseHttpWrapper extends ServletResponseWrapper implements HttpServletResponse { 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 14700779b34..c46b31e342c 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 @@ -50,7 +50,7 @@ import org.eclipse.jetty.util.log.Logger; * Handler for Error pages * An ErrorHandler is registered with {@link ContextHandler#setErrorHandler(ErrorHandler)} or * {@link Server#setErrorHandler(ErrorHandler)}. - * It is called by the HttpResponse.sendError method to write a error page via {@link #handle(String, Request, HttpServletRequest, HttpServletResponse)} + * It is called by the HttpResponse.sendError method to write an error page via {@link #handle(String, Request, HttpServletRequest, HttpServletResponse)} * or via {@link #badMessageError(int, String, HttpFields)} for bad requests for which a dispatch cannot be done. */ public class ErrorHandler extends AbstractHandler @@ -467,7 +467,7 @@ public class ErrorHandler extends AbstractHandler /** * Bad Message Error body - * <p>Generate a error response body to be sent for a bad message. + * <p>Generate an error response body to be sent for a bad message. * In this case there is something wrong with the request, so either * a request cannot be built, or it is not safe to build a request. * This method allows for a simple error page body to be returned diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java index ed5affa51c9..4665db27c89 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java @@ -38,7 +38,7 @@ import org.eclipse.jetty.util.log.Logger; * A handler that shuts the server down on a valid request. Used to do "soft" restarts from Java. * If _exitJvm is set to true a hard System.exit() call is being made. * If _sendShutdownAtStart is set to true, starting the server will try to shut down an existing server at the same port. - * If _sendShutdownAtStart is set to true, make a http call to + * If _sendShutdownAtStart is set to true, make an http call to * "http://localhost:" + port + "/shutdown?token=" + shutdownCookie * in order to shut down the server. * @@ -100,7 +100,7 @@ public class ShutdownHandler extends HandlerWrapper /** * @param shutdownToken a secret password to avoid unauthorized shutdown attempts * @param exitJVM If true, when the shutdown is executed, the handler class System.exit() - * @param sendShutdownAtStart If true, a shutdown is sent as a HTTP post + * @param sendShutdownAtStart If true, a shutdown is sent as an HTTP post * during startup, which will shutdown any previously running instances of * this server with an identically configured ShutdownHandler */ diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpInputInterceptor.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpInputInterceptor.java index 9fedb2b3938..c099e09abfd 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpInputInterceptor.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpInputInterceptor.java @@ -27,7 +27,7 @@ import org.eclipse.jetty.server.HttpInput.Content; import org.eclipse.jetty.util.component.Destroyable; /** - * A HttpInput Interceptor that inflates GZIP encoded request content. + * An HttpInput Interceptor that inflates GZIP encoded request content. */ public class GzipHttpInputInterceptor implements HttpInput.Interceptor, Destroyable { 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 a14d5f5ab53..76d146b2ea3 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 @@ -42,7 +42,7 @@ import org.eclipse.jetty.util.thread.Locker.Lock; /** * Session * - * A heavy-weight Session object representing a HttpSession. Session objects + * A heavy-weight Session object representing an HttpSession. Session objects * relating to a context are kept in a {@link SessionCache}. The purpose of the * SessionCache is to keep the working set of Session objects in memory so that * they may be accessed quickly, and facilitate the sharing of a Session object diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionHandler.java index 89857621904..7c958c465eb 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionHandler.java @@ -555,7 +555,7 @@ public class SessionHandler extends ScopedHandler /** * @return same as SessionCookieConfig.getSecure(). If true, session * cookies are ALWAYS marked as secure. If false, a session cookie is - * ONLY marked as secure if _secureRequestOnly == true and it is a HTTPS request. + * ONLY marked as secure if _secureRequestOnly == true and it is an HTTPS request. */ @ManagedAttribute("if true, secure cookie flag is set on session cookies") public boolean getSecureCookies() diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ThreadStarvationTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ThreadStarvationTest.java index a145c2c2782..15b31826bf5 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ThreadStarvationTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ThreadStarvationTest.java @@ -362,7 +362,7 @@ public class ThreadStarvationTest byte[] content = new byte[BUFFER_SIZE]; { - // Using a character that will not show up in a HTTP response header + // Using a character that will not show up in an HTTP response header Arrays.fill(content, (byte)'!'); } diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/RequestURITest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/RequestURITest.java index adbb8abd27f..2f0195fa494 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/RequestURITest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/RequestURITest.java @@ -196,7 +196,7 @@ public class RequestURITest // Read the response. String response = readResponse(client); - // TODO: is HTTP/1.1 response appropriate for a HTTP/1.0 request? + // TODO: is HTTP/1.1 response appropriate for an HTTP/1.0 request? assertThat(response, Matchers.containsString("HTTP/1.1 200 OK")); assertThat(response, Matchers.containsString("RequestURI: " + expectedReqUri)); assertThat(response, Matchers.containsString("QueryString: " + expectedQuery)); diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/DoSFilter.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/DoSFilter.java index d15f57dc3cc..45aa1b99637 100644 --- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/DoSFilter.java +++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/DoSFilter.java @@ -489,7 +489,7 @@ public class DoSFilter implements Filter /** * Invoked when the request handling exceeds {@link #getMaxRequestMs()}. * <p> - * By default, a HTTP 503 response is returned and the handling thread is interrupted. + * By default, an HTTP 503 response is returned and the handling thread is interrupted. * * @param request the current request * @param response the current response diff --git a/jetty-unixsocket/src/main/config/modules/unixsocket-http.mod b/jetty-unixsocket/src/main/config/modules/unixsocket-http.mod index e8f17427237..8d33f657fef 100644 --- a/jetty-unixsocket/src/main/config/modules/unixsocket-http.mod +++ b/jetty-unixsocket/src/main/config/modules/unixsocket-http.mod @@ -1,7 +1,7 @@ DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html [description] -Adds a HTTP protocol support to the Unix Domain Socket connector. +Adds an HTTP protocol support to the Unix Domain Socket connector. It should be used when a proxy is forwarding either HTTP or decrypted HTTPS traffic to the connector and may be used with the unix-socket-http2c modules to upgrade to HTTP/2. diff --git a/jetty-unixsocket/src/main/config/modules/unixsocket-http2c.mod b/jetty-unixsocket/src/main/config/modules/unixsocket-http2c.mod index 7de5cb019fb..f351a7a8849 100644 --- a/jetty-unixsocket/src/main/config/modules/unixsocket-http2c.mod +++ b/jetty-unixsocket/src/main/config/modules/unixsocket-http2c.mod @@ -1,7 +1,7 @@ DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html [description] -Adds a HTTP2C connetion factory to the Unix Domain Socket Connector +Adds an HTTP2C connetion factory to the Unix Domain Socket Connector It can be used when either the proxy forwards direct HTTP/2C (unecrypted) or decrypted HTTP/2 traffic. diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java index b59b015fdfc..5cebbb37bf5 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java @@ -367,7 +367,7 @@ public abstract class Resource implements ResourceFactory, Closeable /** * URL representing the resource. * - * @return an URL representing the given resource + * @return a URL representing the given resource * @deprecated use {{@link #getURI()}.toURL() instead. */ @Deprecated diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResource.java b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResource.java index cc4916cac76..66c6734df8b 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResource.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResource.java @@ -151,7 +151,7 @@ public class URLResource extends Resource } /** - * Returns an URL representing the given resource + * Returns a URL representing the given resource */ @Override public URL getURL() diff --git a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/util/WSURI.java b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/util/WSURI.java index e48a2de056b..f8840f032da 100644 --- a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/util/WSURI.java +++ b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/util/WSURI.java @@ -25,7 +25,7 @@ import java.util.Objects; // @checkstyle-disable-check : AbbreviationAsWordInNameCheck /** - * Utility methods for converting a {@link URI} between a HTTP(S) and WS(S) URI. + * Utility methods for converting a {@link URI} between an HTTP(S) and WS(S) URI. */ public final class WSURI { diff --git a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketUpgradeFilter.java b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketUpgradeFilter.java index de55c297274..20a16f4f9ba 100644 --- a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketUpgradeFilter.java +++ b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketUpgradeFilter.java @@ -275,7 +275,7 @@ public class WebSocketUpgradeFilter implements Filter, MappedWebSocketCreator, D // We are in some kind of funky non-http environment. if (LOG.isDebugEnabled()) { - LOG.debug("Not a HttpServletRequest, skipping WebSocketUpgradeFilter"); + LOG.debug("Not an HttpServletRequest, skipping WebSocketUpgradeFilter"); } } diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/TooFastClientTest.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/TooFastClientTest.java index 4077e0f27c8..167e30445fa 100644 --- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/TooFastClientTest.java +++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/TooFastClientTest.java @@ -127,7 +127,7 @@ public class TooFastClientTest } /** - * Test where were a client sends a HTTP Upgrade to websocket AND enough websocket frame(s) + * Test where were a client sends an HTTP Upgrade to websocket AND enough websocket frame(s) * to completely overfill the {@link org.eclipse.jetty.io.AbstractConnection#getInputBufferSize()} * to test a situation where the WebSocket connection opens with prefill that exceeds * the normal input buffer sizes. diff --git a/tests/test-distribution/src/main/java/org/eclipse/jetty/tests/distribution/DistributionTester.java b/tests/test-distribution/src/main/java/org/eclipse/jetty/tests/distribution/DistributionTester.java index 2b9178e20b1..13960cfa783 100644 --- a/tests/test-distribution/src/main/java/org/eclipse/jetty/tests/distribution/DistributionTester.java +++ b/tests/test-distribution/src/main/java/org/eclipse/jetty/tests/distribution/DistributionTester.java @@ -97,7 +97,7 @@ import org.eclipse.jetty.util.log.Logger; * // Wait for Jetty to be fully started. * assertTrue(run1.awaitConsoleLogsFor("Started @", 20, TimeUnit.SECONDS)); * - * // Make a HTTP request to the web application. + * // Make an HTTP request to the web application. * HttpClient client = new HttpClient(); * client.start(); * ContentResponse response = client.GET("http://localhost:" + port + "/test/index.jsp");