diff options
| author | Michael Peter Christen <mc@yacy.net> | 2026-07-12 12:03:57 +0200 |
|---|---|---|
| committer | Michael Peter Christen <mc@yacy.net> | 2026-07-12 12:03:57 +0200 |
| commit | 554911a88f8c5084146a8a098965f18e67416525 (patch) | |
| tree | 55012e1b4002e42899b02f51e290925566ae74c8 /test/java/net | |
| parent | b2e9142ea9a61af8c6e55a39a392136306dc0312 (diff) | |
Migration to Jetty 12.1.11
Diffstat (limited to 'test/java/net')
8 files changed, 998 insertions, 386 deletions
diff --git a/test/java/net/yacy/http/AdminSecurityTest.java b/test/java/net/yacy/http/AdminSecurityTest.java index 196eae3ea..02725d72f 100644 --- a/test/java/net/yacy/http/AdminSecurityTest.java +++ b/test/java/net/yacy/http/AdminSecurityTest.java @@ -112,35 +112,35 @@ public class AdminSecurityTest { public void testAdminAccessPolicy() { final String user = "admin"; final String hash = AdminSecurity.calcHash(user + ":secret"); - final AdminAccessPolicy localAllowed = new AdminAccessPolicy( + final AdminSecurity.AccessPolicy localAllowed = new AdminSecurity.AccessPolicy( false, false, true, true, user, hash); - Assert.assertEquals(AdminAccessPolicy.Decision.PUBLIC, + Assert.assertEquals(AdminSecurity.AccessPolicy.Decision.PUBLIC, localAllowed.decide("/index.html", "192.0.2.1", null, null)); - Assert.assertEquals(AdminAccessPolicy.Decision.ADMIN_REQUIRED, + Assert.assertEquals(AdminSecurity.AccessPolicy.Decision.ADMIN_REQUIRED, localAllowed.decide("/Settings_p.html", "192.0.2.1", null, null)); - Assert.assertEquals(AdminAccessPolicy.Decision.LOCAL_BYPASS, + Assert.assertEquals(AdminSecurity.AccessPolicy.Decision.LOCAL_BYPASS, localAllowed.decide("/Settings_p.html", "127.0.0.1", null, null)); - Assert.assertEquals(AdminAccessPolicy.Decision.ADMIN_REQUIRED, + Assert.assertEquals(AdminSecurity.AccessPolicy.Decision.ADMIN_REQUIRED, localAllowed.decide("/Settings_p.html", "127.0.0.1", "https://example.org/", null)); - final AdminAccessPolicy loginRequired = new AdminAccessPolicy( + final AdminSecurity.AccessPolicy loginRequired = new AdminSecurity.AccessPolicy( false, false, true, false, user, hash); - Assert.assertEquals(AdminAccessPolicy.Decision.ADMIN_REQUIRED, + Assert.assertEquals(AdminSecurity.AccessPolicy.Decision.ADMIN_REQUIRED, loginRequired.decide("/Settings_p.html", "127.0.0.1", null, null)); final String lazyAuth = "Basic " + Base64Order.standardCoder.encodeString(user + ":" + hash); - Assert.assertEquals(AdminAccessPolicy.Decision.LOCAL_BYPASS, + Assert.assertEquals(AdminSecurity.AccessPolicy.Decision.LOCAL_BYPASS, loginRequired.decide("/Settings_p.html", "127.0.0.1", null, lazyAuth)); } /** The credential context is request-bound and fails closed after cleanup. */ @Test public void testAdminAuthenticationContext() { - AdminAuthenticationContext.clear(); - Assert.assertFalse(AdminAuthenticationContext.isLocalhostRequest()); - AdminAuthenticationContext.setSocketPeerIp("127.0.0.1"); - Assert.assertTrue(AdminAuthenticationContext.isLocalhostRequest()); - AdminAuthenticationContext.clear(); - Assert.assertFalse(AdminAuthenticationContext.isLocalhostRequest()); + AdminSecurity.AuthenticationContext.clear(); + Assert.assertFalse(AdminSecurity.AuthenticationContext.isLocalhostRequest()); + AdminSecurity.AuthenticationContext.setSocketPeerIp("127.0.0.1"); + Assert.assertTrue(AdminSecurity.AuthenticationContext.isLocalhostRequest()); + AdminSecurity.AuthenticationContext.clear(); + Assert.assertFalse(AdminSecurity.AuthenticationContext.isLocalhostRequest()); } } diff --git a/test/java/net/yacy/http/HttpServerBootstrapConfigTest.java b/test/java/net/yacy/http/HttpServerBootstrapConfigTest.java index 7f2f71120..9faecb2fd 100644 --- a/test/java/net/yacy/http/HttpServerBootstrapConfigTest.java +++ b/test/java/net/yacy/http/HttpServerBootstrapConfigTest.java @@ -20,6 +20,6 @@ public class HttpServerBootstrapConfigTest { Assert.assertEquals(9_000L, HttpServerBootstrapConfig.CONNECTOR_IDLE_TIMEOUT_MILLIS); Assert.assertEquals(128, HttpServerBootstrapConfig.ACCEPT_QUEUE_SIZE); Assert.assertEquals(4_096, HttpServerBootstrapConfig.REQUEST_INFLATE_BUFFER_SIZE); - Assert.assertEquals(-1, HttpServerBootstrapConfig.MAX_FORM_CONTENT_SIZE); + Assert.assertEquals(200_000, HttpServerBootstrapConfig.MAX_FORM_CONTENT_SIZE); } } diff --git a/test/java/net/yacy/http/InetPathAccessHandlerTest.java b/test/java/net/yacy/http/InetPathAccessHandlerTest.java deleted file mode 100644 index 641ad8bf1..000000000 --- a/test/java/net/yacy/http/InetPathAccessHandlerTest.java +++ /dev/null @@ -1,355 +0,0 @@ -// InetPathAccessHandlerTest.java -// Copyright 2017 by luccioman; https://github.com/luccioman -// -// This is a part of YaCy, a peer-to-peer based web search engine -// -// LICENSE -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -package net.yacy.http; - -import java.net.InetAddress; -import java.net.UnknownHostException; - -import org.junit.Assert; -import org.junit.Test; - -/** - * Unit tests for the {@link InetPathAccessHandler} class. - */ -public class InetPathAccessHandlerTest { - - @Test - public void testPortableRuleParsing() { - final InetPathAccessRule addressOnly = InetPathAccessRule.parse("192.168.1.0/24"); - Assert.assertEquals("192.168.1.0/24", addressOnly.addressPattern()); - Assert.assertEquals("/*", addressOnly.pathPattern()); - - final InetPathAccessRule addressAndPath = InetPathAccessRule.parse("127.0.0.1|/api/*"); - Assert.assertEquals("127.0.0.1", addressAndPath.addressPattern()); - Assert.assertEquals("/api/*", addressAndPath.pathPattern()); - Assert.assertEquals("127.0.0.1|/api/*", addressAndPath.asJettyPattern()); - } - - /** - * Check the handler allow the given ip/path pairs. - * - * @param handler - * the handler to test. Must not be null. - * @param ipAndPaths - * array of ip address and path pairs. Must not be null. - * @throws UnknownHostException - * when a test address is incorrect. - */ - private void assertAllowed(final InetPathAccessHandler handler, final String[][] ipAndPaths) - throws UnknownHostException { - for (final String[] ipAndPath : ipAndPaths) { - final String ip = ipAndPath[0]; - final String path = ipAndPath[1]; - Assert.assertTrue("Should allow " + ip + path, handler.isAllowed(InetAddress.getByName(ip), path)); - } - } - - /** - * Check the handler dos not allow the given ip/path pairs. - * - * @param handler - * the handler to test. Must not be null. - * @param ipAndPaths - * array of ip address and path pairs. Must not be null. - * @throws UnknownHostException - * when a test address is incorrect. - */ - private void assertRejected(final InetPathAccessHandler handler, final String[][] ipAndPaths) - throws UnknownHostException { - for (final String[] ipAndPath : ipAndPaths) { - final String ip = ipAndPath[0]; - final String path = ipAndPath[1]; - Assert.assertFalse("Should not allow " + ip + path, handler.isAllowed(InetAddress.getByName(ip), path)); - } - } - - /** - * Test inclusion with a single white listed IPv4 address. - * - * @throws UnknownHostException - * when a test address is incorrect. Should not happen. - */ - @Test - public void testIncludeSingleIPv4() throws UnknownHostException { - final InetPathAccessHandler handler = new InetPathAccessHandler(); - handler.include("10.10.1.2"); - - final String[][] allowed = { { "10.10.1.2", "/" }, // matching address, root path - { "10.10.1.2", "/foo/bar" }, // matching address, non root path - { "10.10.1.2", null } // matching address, no path information provided - }; - this.assertAllowed(handler, allowed); - - final String[][] rejected = { { "10.10.1.3", "/" }, // non matching address, root path - { null, null } // no address nor path information provided - }; - this.assertRejected(handler, rejected); - } - - /** - * Test inclusion with a single white listed IPv6 address. - * - * @throws UnknownHostException - * when a test address is incorrect. Should not happen. - */ - @Test - public void testIncludeSingleIPv6() throws UnknownHostException { - final InetPathAccessHandler handler = new InetPathAccessHandler(); - handler.include("2001:db8::ff00:42:8329"); - - final String[][] allowed = { { "2001:db8::ff00:42:8329", "/" }, // matching address, root path - { "2001:0db8:0000:0000:0000:ff00:0042:8329", "/" }, // matching address in long representation, root - // path - { "2001:db8::ff00:42:8329", "/foo/bar" }, // matching address, non root path - { "2001:db8::ff00:42:8329", null } // matching address, no path information provided - }; - this.assertAllowed(handler, allowed); - - final String[][] rejected = { { "2001:db8::ff00:42:8539", "/" }, // non matching address, root path - { null, null } // no address nor path information provided - }; - this.assertRejected(handler, rejected); - } - - /** - * Test inclusion with a single white listed IPV4 address and path. - * - * @throws UnknownHostException - * when a test address is incorrect. Should not happen. - */ - @Test - public void testIncludeSingleAddressAndPath() throws UnknownHostException { - final InetPathAccessHandler handler = new InetPathAccessHandler(); - handler.include("10.10.1.2|/foo/bar"); - - final String[][] allowed = { { "10.10.1.2", "/foo/bar" } // matching address, matching path - }; - this.assertAllowed(handler, allowed); - - final String[][] rejected = { { "10.10.1.3", "/" }, // non matching address, non matching path - { "10.10.1.3", "/foo/bar" }, // non matching address, even if matching path - { "10.10.1.2", "/" }, // matching address, but non matching root path - { "10.10.1.2", "/foo" }, // matching address, but non matching parent path - { "10.10.1.2", "/foo/" }, // matching address, but non matching parent path - { "10.10.1.2", "/foo/wrong" }, // matching address, but non matching sub path - { "10.10.1.2", "/foo/bar/file.txt" } // matching address, but non matching sub path with file - }; - this.assertRejected(handler, rejected); - } - - /** - * Test inclusion with a single white listed IPV4 address and wildcard path. - * - * @throws UnknownHostException - * when a test address is incorrect. Should not happen. - */ - @Test - public void testIncludeSingleAddressAndWildcardPath() throws UnknownHostException { - final InetPathAccessHandler handler = new InetPathAccessHandler(); - handler.include("10.10.1.2|/foo/*"); - - final String[][] allowed = { { "10.10.1.2", "/foo/bar" }, // matching address, matching sub path - { "10.10.1.2", "/foo/bar/sub" }, // matching address, matching sub path - { "10.10.1.2", "/foo/file.txt" }, // matching address, matching sub path with file - { "10.10.1.2", "/foo" }, // matching address, matching path - }; - this.assertAllowed(handler, allowed); - - final String[][] rejected = { { "10.10.1.3", "/" }, // non matching address, non matching path - { "10.10.1.3", "/foo/bar" }, // non matching address, event if matching path - { "10.10.1.2", "/" }, // matching address, but non matching root path - { "10.10.1.2", null }, // matching address, but no path information provided - { null, "/foo/bar" } // no address provided, event if matching path - }; - this.assertRejected(handler, rejected); - } - - /** - * Test inclusion with a single white listed IPV4 address and wildcard path - * suffix. - * - * @throws UnknownHostException - * when a test address is incorrect. Should not happen. - */ - @Test - public void testIncludeSingleAddressAndWildcardSuffix() throws UnknownHostException { - final InetPathAccessHandler handler = new InetPathAccessHandler(); - handler.include("10.10.1.2|*.html"); - - final String[][] allowed = { { "10.10.1.2", "/index.html" }, // matching address, matching file path - { "10.10.1.2", "/foo/bar/index.html" }, // matching address, matching file with parent path - }; - this.assertAllowed(handler, allowed); - - final String[][] rejected = { { "10.10.1.3", "/" }, // non matching address, non matching path - { "10.10.1.3", "/index.html" }, // non matching address, event if matching file path - { "10.10.1.2", "/" }, // matching address, but non matching root path - { "10.10.1.2", "/index.txt" }, // matching address, but non matching file path - { "10.10.1.2", null }, // matching address, but no path information provided - { null, "/index.html" } // no address provided, event if matching path - }; - this.assertRejected(handler, rejected); - } - - /** - * Test inclusion with ranges of white listed addresses. - * - * @throws UnknownHostException - * when a test address is incorrect. Should not happen. - */ - @Test - public void testIncludeRanges() throws UnknownHostException { - final InetPathAccessHandler handler = new InetPathAccessHandler(); - handler.include("10.10.1.1-255"); // legacy IPv4 range format used by IPAddressMap - handler.include("192.168.128.0-192.168.128.255"); // inclusive range of IPv4 addresses - handler.include("2001:db8::ff00:42:8329-2001:db8::ff00:42:ffff"); // inclusive range of IPv6 addresses - handler.include("192.168.1.0/24"); // CIDR notation on IPv4 - handler.include("2001:db8::aaaa:0:0/96"); // CIDR notation on IPv6 - - final String[][] allowed = { { "10.10.1.1", "/" }, // matching legacy IPv4 range - { "10.10.1.255", "/" }, // matching legacy IPv4 range - { "192.168.128.0", "/" }, // matching second range of IPv4 addresses - { "192.168.128.255", "/" }, // matching second range of IPv4 addresses - { "2001:db8::ff00:42:8329", "/" }, // matching IPv6 range - { "2001:db8::ff00:42:99ff", "/" }, // matching IPv6 range - { "192.168.1.0", "/" }, // matching IPv4 CIDR notation range - { "192.168.1.255", "/" }, // matching IPv4 CIDR notation range - { "2001:db8::aaaa:1:1", "/" }, // matching IPv6 CIDR notation range - { "2001:db8::aaaa:ffff:ffff", "/" } // matching IPv6 CIDR notation range - }; - this.assertAllowed(handler, allowed); - - final String[][] rejected = { { "10.9.1.1", "/" }, { "10.10.2.1", "/" }, { "192.168.127.1", "/" }, - { "2001:db8::ff00:43:1234", "/" }, { "192.168.2.1", "/" }, { "2001:db8::aabb:ffff:ffff", "/" } }; - this.assertRejected(handler, rejected); - } - - /** - * Test inclusion with ranges of white listed addresses associated with wildcard - * paths. - * - * @throws UnknownHostException - * when a test address is incorrect. Should not happen. - */ - @Test - public void testIncludeRangesAndWildcardPaths() throws UnknownHostException { - final InetPathAccessHandler handler = new InetPathAccessHandler(); - handler.include("10.10.1.1-255|/foo/*"); // legacy IPv4 range format used by IPAddressMap - handler.include("192.168.128.0-192.168.128.255|/path/*"); // inclusive range of IPv4 adresses - handler.include("2001:db8::ff00:42:8329-2001:db8::ff00:42:ffff|/root/*"); // inclusive range of IPv6 adresses - handler.include("192.168.1.0/24|/www/*"); // CIDR notation - - final String[][] allowed = { { "10.10.1.1", "/foo/bar" }, // matching legacy IPv4 range and path - { "10.10.1.255", "/foo/bar" }, // matching legacy IPv4 range and path - { "192.168.128.0", "/path/index.html" }, // matching second range of IPv4 addresses and path - { "192.168.128.255", "/path/file.txt" }, // matching second range of IPv4 addresses and path - { "2001:db8::ff00:42:8329", "/root/index.txt" }, // matching IPv6 range and path - { "2001:db8::ff00:42:99ff", "/root/image.jpg" }, // matching IPv6 range and path - { "192.168.1.0", "/www/resource" }, // matching IPv4 CIDR notation range and path - { "192.168.1.255", "/www/home" } }; // matching IPv4 CIDR notation range and path - this.assertAllowed(handler, allowed); - - final String[][] rejected = { { "10.9.1.1", "/" }, { "10.9.1.1", "/foo/bar" }, { "10.10.2.1", "/" }, - { "10.10.2.1", "/foo/bar" }, { "192.168.127.1", "/" }, { "192.168.127.1", "/path/index.html" }, - { "2001:db8::ff00:43:1234", "/" }, { "2001:db8::ff00:43:1234", "/root/index.txt" }, - { "192.168.2.1", "/" }, { "192.168.2.1", "/www/content" } }; - this.assertRejected(handler, rejected); - } - - /** - * Test inclusion with multiple patterns using the same path - * - * @throws UnknownHostException - * when a test address is incorrect. Should not happen. - */ - @Test - public void testIncludeMultiplePatternsOnSamePath() throws UnknownHostException { - final InetPathAccessHandler handler = new InetPathAccessHandler(); - handler.include("10.10.1.1|/foo/bar"); // a single address pattern - handler.include("192.168.128.0-192.168.128.255|/foo/bar"); // inclusive range of IPv4 adresses - - final String[][] allowed = { { "10.10.1.1", "/foo/bar" }, // matching single address pattern - { "192.168.128.0", "/foo/bar" }, { "192.168.128.255", "/foo/bar" } // matching range pattern - }; - this.assertAllowed(handler, allowed); - - final String[][] rejected = { { "10.10.1.1", "/" }, // matching single address pattern bu root path - { "127.0.0.1", "/" }, // non matching address - }; - this.assertRejected(handler, rejected); - } - - /** - * Test exclusion with a single white listed IPV4 address and path. - * - * @throws UnknownHostException - * when a test address is incorrect. Should not happen. - */ - @Test - public void testExcludeSingleAddressAndPath() throws UnknownHostException { - final InetPathAccessHandler handler = new InetPathAccessHandler(); - handler.exclude("10.10.1.2|/foo/bar"); - - final String[][] allowed = { { "10.10.1.3", "/" }, // non matching address, non matching path - { "10.10.1.3", "/foo/bar" }, // non matching address, even if matching path - { "10.10.1.2", "/" }, // matching address, but non matching root path - { "10.10.1.2", "/foo" }, // matching address, but non matching parent path - { "10.10.1.2", "/foo/" }, // matching address, but non matching parent path - { "10.10.1.2", "/foo/wrong" }, // matching address, but non matching sub path - { "10.10.1.2", "/foo/bar/file.txt" } // matching address, but non matching sub path with file - }; - - this.assertAllowed(handler, allowed); - - final String[][] rejected = { { "10.10.1.2", "/foo/bar" } // matching address, matching path - }; - - this.assertRejected(handler, rejected); - } - - /** - * Test inclusion and exclusion rules applied on the same address - * - * @throws UnknownHostException - * when a test address is incorrect. Should not happen. - */ - @Test - public void testIncludeExcludeOnSameAddress() throws UnknownHostException { - final InetPathAccessHandler handler = new InetPathAccessHandler(); - handler.include("10.10.1.1-10.10.1.255"); // include a range of addresses without path restrictions - handler.exclude("10.10.1.2|/foo/bar"); // exclude a specific address and path - - final String[][] allowed = { { "10.10.1.3", "/" }, // matching included addresses range - { "10.10.1.2", "/" }, // matching excluded address, but non matching root path - { "10.10.1.2", "/foo" }, // matching excluded address, but non matching parent path - { "10.10.1.2", "/foo/wrong" }, // matching excluded address, but non matching sub path - { "10.10.1.2", "/foo/bar/file.txt" } // matching excluded address, but non matching sub path with file - }; - - this.assertAllowed(handler, allowed); - - final String[][] rejected = { { "10.10.1.2", "/foo/bar" } // matching excluded address and path - }; - - this.assertRejected(handler, rejected); - } -} diff --git a/test/java/net/yacy/http/Jetty12HttpServerTest.java b/test/java/net/yacy/http/Jetty12HttpServerTest.java new file mode 100644 index 000000000..160188358 --- /dev/null +++ b/test/java/net/yacy/http/Jetty12HttpServerTest.java @@ -0,0 +1,427 @@ +package net.yacy.http; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +import javax.net.ssl.SSLContext; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.compression.server.CompressionConfig; +import org.eclipse.jetty.ee8.security.authentication.DigestAuthenticator; +import org.eclipse.jetty.ee8.servlet.ServletContextHandler; +import org.eclipse.jetty.ee8.servlet.ServletHolder; +import org.eclipse.jetty.security.UserIdentity; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.InetAccessHandler; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.security.Credential; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; + +import net.yacy.cora.order.Digest; +import net.yacy.search.SwitchboardConstants; + +/** + * Focused tests for the consolidated {@link Jetty12HttpServer} adapter and its + * nested default-runtime handlers. The transparent-proxy chain is covered by + * {@link Jetty12ProxyChainTest}. + */ +@RunWith(Enclosed.class) +public class Jetty12HttpServerTest { + + /** Lifecycle, connector and compression contract of the bootstrap. */ + public static class ServerLifecycleTest { + + @Test + public void startsAndStopsHttpConnectorOnEphemeralPort() throws Exception { + final Logger jettyLogger = Logger.getLogger("org.eclipse.jetty.server.Server"); + final AtomicBoolean receivedJettyLog = new AtomicBoolean(); + final java.util.logging.Handler capture = new java.util.logging.Handler() { + @Override + public void publish(final LogRecord record) { + receivedJettyLog.set(true); + } + + @Override + public void flush() { + } + + @Override + public void close() { + } + }; + capture.setLevel(Level.ALL); + jettyLogger.addHandler(capture); + final Jetty12HttpServer server = new Jetty12HttpServer( + 0, "127.0.0.1", 1, null, -1); + assertFalse(server.withSSL()); + assertEquals(-1, server.getSslPort()); + assertTrue(server.getVersion().startsWith("Jetty 12.1.11")); + + try { + server.startupServer(); + assertTrue(server.getHttpPort() > 0); + assertTrue(server.getServerThreads() >= 0); + } finally { + server.stop(); + jettyLogger.removeHandler(capture); + } + assertTrue("Jetty 12 lifecycle log did not reach JUL", receivedJettyLog.get()); + } + + @Test + public void configuresHttpsConnectorWhenSslContextIsAvailable() throws Exception { + final Jetty12HttpServer server = new Jetty12HttpServer( + 0, "127.0.0.1", 1, SSLContext.getDefault(), 0); + assertTrue(server.withSSL()); + try { + server.startupServer(); + assertTrue(server.getHttpPort() > 0); + assertTrue(server.getSslPort() > 0); + } finally { + server.stop(); + } + } + + @Test + public void acceptsConfiguredCertificatesWithoutJetty12SniHostRejection() { + final SecureRequestCustomizer secureRequests = + Jetty12HttpServer.createSecureRequestCustomizer(); + assertFalse(secureRequests.isSniHostCheck()); + assertFalse(secureRequests.isSniRequired()); + } + + @Test + public void preservesJetty9SvgCompressionContract() { + final CompressionConfig compression = Jetty12HttpServer.createCompressionConfig(true); + assertTrue(compression.isCompressMimeTypeSupported("image/svg+xml")); + assertFalse(compression.isCompressMimeTypeSupported("image/png")); + assertTrue(compression.isCompressMethodSupported("GET")); + } + + @Test + public void preservesJetty9WebApplicationFormContentLimit() throws Exception { + final Server server = new Server(); + final ServerConnector connector = new ServerConnector(server, 1, 1); + connector.setHost("127.0.0.1"); + connector.setPort(0); + server.addConnector(connector); + + final ServletContextHandler webApp = new ServletContextHandler(); + webApp.setContextPath("/"); + webApp.setMaxFormContentSize(HttpServerBootstrapConfig.MAX_FORM_CONTENT_SIZE); + webApp.addServlet(new ServletHolder(new HttpServlet() { + private static final long serialVersionUID = 1L; + + @Override + protected void doPost(final HttpServletRequest request, + final HttpServletResponse response) throws java.io.IOException { + response.setStatus(HttpServletResponse.SC_OK); + response.getWriter().print(request.getParameter("value").length()); + } + }), "/*"); + server.setHandler(webApp); + + try { + server.start(); + final String accepted = postForm(connector.getLocalPort(), "value=" + "a".repeat(1024)); + assertTrue(accepted, accepted.startsWith("HTTP/1.1 200")); + assertTrue(accepted, accepted.contains("1024")); + + final String rejected = postForm(connector.getLocalPort(), + "value=" + "a".repeat(HttpServerBootstrapConfig.MAX_FORM_CONTENT_SIZE)); + assertTrue(rejected, rejected.startsWith("HTTP/1.1 400")); + } finally { + server.stop(); + server.join(); + } + } + + private static String postForm(final int port, final String body) throws Exception { + final byte[] content = body.getBytes(StandardCharsets.US_ASCII); + try (Socket client = new Socket("127.0.0.1", port)) { + final OutputStream output = client.getOutputStream(); + output.write(("POST / HTTP/1.1\r\nHost: 127.0.0.1:" + port + + "\r\nContent-Type: application/x-www-form-urlencoded" + + "\r\nContent-Length: " + content.length + + "\r\nConnection: close\r\n\r\n").getBytes(StandardCharsets.US_ASCII)); + output.write(content); + output.flush(); + return readAll(client.getInputStream()); + } + } + } + + /** Address/path rule adaptation to Jetty 12's native access handler. */ + public static class AccessRulesTest { + + private static final Handler NEXT = new Handler.Abstract() { + @Override + public boolean handle(final Request request, final Response response, final Callback callback) { + return false; + } + }; + + @Test + public void leavesUnrestrictedPipelineUntouched() { + assertSame(NEXT, Jetty12HttpServer.AccessRules.wrap(NEXT, "*")); + } + + @Test + public void usesNativeJetty12HandlerForAddressAndPathRule() { + final Handler wrapped = Jetty12HttpServer.AccessRules.wrap(NEXT, "192.0.2.0/24|/api/*"); + assertTrue(wrapped instanceof InetAccessHandler); + assertSame(NEXT, ((InetAccessHandler) wrapped).getHandler()); + } + + @Test + public void ignoresEmptyRuleList() { + assertSame(NEXT, Jetty12HttpServer.AccessRules.wrap(NEXT, " , ")); + } + + @Test + public void validatesConfiguredAddressAndPathPattern() { + Jetty12HttpServer.AccessRules.checkPattern("192.0.2.0/24|/api/*"); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsMalformedConfiguredAddressPattern() { + Jetty12HttpServer.AccessRules.checkPattern("not an address|/api/*"); + } + } + + /** Administrator credential verification for BASIC and DIGEST. */ + public static class AdminCredentialTest { + + @Test + public void verifiesBasicPasswordAgainstRealmHash() { + final String hash = "MD5:" + Digest.encodeMD5Hex("admin:YaCy:test-password"); + final Jetty12HttpServer.AdminCredential credential = new Jetty12HttpServer.AdminCredential( + "admin", hash, "YaCy", "admin"); + assertTrue(credential.check("test-password")); + assertFalse(credential.check("wrong-password")); + } + + @Test + public void acceptsConfiguredHashAsPasswordOnlyFromLocalhost() { + final String hash = "MD5:" + Digest.encodeMD5Hex("admin:YaCy:test-password"); + final Jetty12HttpServer.AdminCredential remoteCredential = new Jetty12HttpServer.AdminCredential( + "admin", hash, "YaCy", "admin", () -> false); + final Jetty12HttpServer.AdminCredential localCredential = new Jetty12HttpServer.AdminCredential( + "admin", hash, "YaCy", "admin", () -> true); + assertFalse(remoteCredential.check(hash)); + assertTrue(localCredential.check(hash)); + } + + @Test + public void acceptsJettyDigestCredentialForConfiguredDigest() { + final String hash = "MD5:" + Digest.encodeMD5Hex("admin:YaCy:test-password"); + final Jetty12HttpServer.AdminCredential credential = new Jetty12HttpServer.AdminCredential( + "admin", hash, "YaCy", "admin"); + assertTrue(credential.check(Credential.getCredential(hash))); + } + + @Test + public void digestChallengeUsesTheAlgorithmOfYacysStoredHa1() { + final DigestAuthenticator authenticator = + Jetty12HttpServer.AdminSecurityHandler.createDigestAuthenticator(); + assertEquals("MD5", authenticator.getAlgorithm()); + } + } + + /** Administrator login-service cache and credential reload contract. */ + public static class AdminLoginServiceTest { + + @Test + public void reloadsChangedAdministratorCredentialAndRole() throws Exception { + final AtomicReference<Jetty12HttpServer.AdminLoginService.AdminCredentialConfig> configured = + new AtomicReference<>(credentials("first-password")); + final Jetty12HttpServer.AdminLoginService service = + new Jetty12HttpServer.AdminLoginService(configured::get); + service.setName("YaCy"); + service.start(); + try { + final UserIdentity first = service.login("admin", "first-password", null, ignored -> null); + assertNotNull(first); + assertTrue(first.isUserInRole(SwitchboardConstants.ADMIN_ACCOUNT_ROLE)); + assertTrue(service.removeCachedUser("admin")); + assertFalse(service.removeCachedUser("admin")); + + configured.set(credentials("second-password")); + service.reloadUser("admin"); + assertNull(service.login("admin", "first-password", null, ignored -> null)); + assertNotNull(service.login("admin", "second-password", null, ignored -> null)); + } finally { + service.stop(); + } + } + + private static Jetty12HttpServer.AdminLoginService.AdminCredentialConfig credentials( + final String password) { + return new Jetty12HttpServer.AdminLoginService.AdminCredentialConfig("admin", + "MD5:" + Digest.encodeMD5Hex("admin:YaCy:" + password), "YaCy"); + } + } + + /** Exception barrier around the complete request pipeline. */ + public static class CrashProtectionHandlerTest { + + @Test + public void convertsSynchronousHandlerFailureTo500Response() throws Exception { + final Server server = new Server(); + final ServerConnector connector = new ServerConnector(server, 1, 1); + connector.setHost("127.0.0.1"); + connector.setPort(0); + server.addConnector(connector); + final Handler failing = new Handler.Abstract() { + @Override + public boolean handle(final Request request, final Response response, + final Callback callback) { + throw new IllegalStateException("expected test failure"); + } + }; + server.setHandler(new Jetty12HttpServer.CrashProtectionHandler(failing)); + try { + server.start(); + final java.net.http.HttpResponse<String> response = + java.net.http.HttpClient.newHttpClient().send( + java.net.http.HttpRequest.newBuilder(java.net.URI.create( + "http://127.0.0.1:" + connector.getLocalPort() + "/failure")) + .GET().build(), + java.net.http.HttpResponse.BodyHandlers.ofString()); + assertEquals(500, response.statusCode()); + } finally { + server.stop(); + server.join(); + } + } + } + + /** CONNECT rejection while normal traffic passes when the proxy is disabled. */ + public static class DisabledProxyHandlerTest { + + @Test + public void passesReverseProxyHostToWebApplication() throws Exception { + final Handler handler = new Jetty12HttpServer.DisabledProxyHandler(fallback()); + final String response = request(handler, + "GET /resource HTTP/1.1\r\n" + + "Host: search.example.org\r\nConnection: close\r\n\r\n"); + assertTrue(response, response.startsWith("HTTP/1.1 204")); + } + + @Test + public void rejectsConnectWith403() throws Exception { + final Handler handler = new Jetty12HttpServer.DisabledProxyHandler(fallback()); + final String response = requestHeaders(handler, + "CONNECT example.invalid:443 HTTP/1.1\r\n" + + "Host: example.invalid:443\r\nConnection: close\r\n\r\n"); + assertTrue(response, response.startsWith("HTTP/1.1 403")); + assertTrue(response, response.contains( + "X-YaCy-Proxy-Error: Transparent proxy is disabled")); + } + + @Test + public void passesDirectPeerRequestToWebApplication() throws Exception { + final Handler handler = new Jetty12HttpServer.DisabledProxyHandler(fallback()); + final String response = request(handler, + "GET /ConfigBasic.html HTTP/1.1\r\n" + + "Host: 127.0.0.1:%d\r\nConnection: close\r\n\r\n"); + assertTrue(response, response.startsWith("HTTP/1.1 204")); + } + + private static Handler fallback() { + return new Handler.Abstract() { + @Override + public boolean handle(final Request request, final Response response, + final Callback callback) { + response.setStatus(204); + callback.succeeded(); + return true; + } + }; + } + + private static String request(final Handler handler, final String requestTemplate) + throws Exception { + return request(handler, requestTemplate, false); + } + + private static String requestHeaders(final Handler handler, final String requestTemplate) + throws Exception { + return request(handler, requestTemplate, true); + } + + private static String request(final Handler handler, final String requestTemplate, + final boolean headersOnly) throws Exception { + final Server server = new Server(); + final ServerConnector connector = new ServerConnector(server, 1, 1); + connector.setHost("127.0.0.1"); + connector.setPort(0); + server.addConnector(connector); + server.setHandler(handler); + try { + server.start(); + try (Socket client = new Socket("127.0.0.1", connector.getLocalPort())) { + final OutputStream output = client.getOutputStream(); + output.write(String.format(requestTemplate, connector.getLocalPort()) + .getBytes(StandardCharsets.US_ASCII)); + output.flush(); + return headersOnly ? readHeaders(client.getInputStream()) + : readAll(client.getInputStream()); + } + } finally { + server.stop(); + server.join(); + } + } + + private static String readHeaders(final InputStream input) throws Exception { + final ByteArrayOutputStream result = new ByteArrayOutputStream(); + int state = 0; + int value; + while ((value = input.read()) >= 0) { + result.write(value); + state = switch (state) { + case 0 -> value == '\r' ? 1 : 0; + case 1 -> value == '\n' ? 2 : 0; + case 2 -> value == '\r' ? 3 : 0; + case 3 -> value == '\n' ? 4 : 0; + default -> state; + }; + if (state == 4) { + break; + } + } + return result.toString(StandardCharsets.ISO_8859_1); + } + } + + static String readAll(final InputStream input) throws Exception { + final ByteArrayOutputStream result = new ByteArrayOutputStream(); + input.transferTo(result); + return result.toString(StandardCharsets.ISO_8859_1); + } +} diff --git a/test/java/net/yacy/http/Jetty12ProxyChainTest.java b/test/java/net/yacy/http/Jetty12ProxyChainTest.java new file mode 100644 index 000000000..8cf811d5c --- /dev/null +++ b/test/java/net/yacy/http/Jetty12ProxyChainTest.java @@ -0,0 +1,457 @@ +package net.yacy.http; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.Callback; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; + +import com.sun.net.httpserver.HttpServer; + +import net.yacy.server.http.AlternativeDomainNames; + +/** + * Focused tests for the consolidated {@link Jetty12ProxyChain} transparent-proxy + * handlers. The default-runtime adapter is covered by {@link Jetty12HttpServerTest}. + */ +@RunWith(Enclosed.class) +public class Jetty12ProxyChainTest { + + /** Single policy gate in front of the cache/forward sequence. */ + public static class ProxyPolicyHandlerTest { + + @Test + public void numericLoopbackIsRecognizedAsThisHost() throws Exception { + assertDirectPeerRequest("/api/version.xml", true); + } + + @Test + public void numericLoopbackOnAnotherPortRemainsAProxyTarget() throws Exception { + assertDirectPeerRequest("http://127.0.0.1:9/resource", false); + } + + @Test + public void unresolvedAbsoluteHostRemainsAProxyTarget() throws Exception { + assertDirectPeerRequest("http://example.invalid/resource", false); + } + + @Test + public void directRequestFallsThroughWithoutPermissionOrProxyPipeline() throws Exception { + final AtomicInteger classification = new AtomicInteger(); + final AtomicInteger permission = new AtomicInteger(); + final AtomicInteger proxyCalls = new AtomicInteger(); + final Handler proxy = responseHandler(202, proxyCalls); + final Handler gate = new Jetty12ProxyChain.ProxyPolicyHandler(proxy, request -> { + classification.incrementAndGet(); + return false; + }, request -> { + permission.incrementAndGet(); + return null; + }); + final String response = request(new Handler.Sequence(gate, responseHandler(204, null)), + "GET /ConfigBasic.html HTTP/1.1\r\nHost: 127.0.0.1:%d\r\n" + + "Connection: close\r\n\r\n"); + assertTrue(response, response.startsWith("HTTP/1.1 204")); + assertEquals(1, classification.get()); + assertEquals(0, permission.get()); + assertEquals(0, proxyCalls.get()); + } + + @Test + public void rejectedRequestNeverReachesProxyPipeline() throws Exception { + final AtomicInteger permission = new AtomicInteger(); + final AtomicInteger proxyCalls = new AtomicInteger(); + final Handler gate = new Jetty12ProxyChain.ProxyPolicyHandler( + responseHandler(202, proxyCalls), + request -> true, request -> { + permission.incrementAndGet(); + return "proxy use not granted"; + }); + final String response = request(gate, + "GET http://example.invalid/resource HTTP/1.1\r\n" + + "Host: example.invalid\r\nConnection: close\r\n\r\n"); + assertTrue(response, response.startsWith("HTTP/1.1 403")); + assertEquals(1, permission.get()); + assertEquals(0, proxyCalls.get()); + } + + @Test + public void cacheHitEvaluatesPolicyOnceAndSkipsForwarding() throws Exception { + final AtomicInteger classification = new AtomicInteger(); + final AtomicInteger permission = new AtomicInteger(); + final AtomicInteger forwarding = new AtomicInteger(); + final HttpFields headers = HttpFields.build().put("Content-Type", "text/plain").asImmutable(); + final Handler cache = new Jetty12ProxyChain.ProxyCacheHandler(request -> + new Jetty12ProxyChain.ProxyCacheHandler.CachedResponse(headers, + "cached".getBytes(StandardCharsets.UTF_8))); + final Handler proxyPipeline = new Handler.Sequence(cache, responseHandler(202, forwarding)); + final Handler gate = countingGate(proxyPipeline, classification, permission); + + final String response = proxyRequest(gate); + assertTrue(response, response.startsWith("HTTP/1.1 203")); + assertEquals(1, classification.get()); + assertEquals(1, permission.get()); + assertEquals(0, forwarding.get()); + } + + @Test + public void cacheMissEvaluatesPolicyOnceBeforeForwarding() throws Exception { + final AtomicInteger classification = new AtomicInteger(); + final AtomicInteger permission = new AtomicInteger(); + final AtomicInteger lookups = new AtomicInteger(); + final AtomicInteger forwarding = new AtomicInteger(); + final Handler cache = new Jetty12ProxyChain.ProxyCacheHandler(request -> { + lookups.incrementAndGet(); + return null; + }); + final Handler proxyPipeline = new Handler.Sequence(cache, responseHandler(202, forwarding)); + final Handler gate = countingGate(proxyPipeline, classification, permission); + + final String response = proxyRequest(gate); + assertTrue(response, response.startsWith("HTTP/1.1 202")); + assertEquals(1, classification.get()); + assertEquals(1, permission.get()); + assertEquals(1, lookups.get()); + assertEquals(1, forwarding.get()); + } + + private static Handler responseHandler(final int status, final AtomicInteger calls) { + return new Handler.Abstract() { + @Override + public boolean handle(final Request request, final Response response, + final Callback callback) { + if (calls != null) { + calls.incrementAndGet(); + } + response.setStatus(status); + callback.succeeded(); + return true; + } + }; + } + + private static Handler countingGate(final Handler proxyPipeline, + final AtomicInteger classification, final AtomicInteger permission) { + return new Jetty12ProxyChain.ProxyPolicyHandler(proxyPipeline, request -> { + classification.incrementAndGet(); + return true; + }, request -> { + permission.incrementAndGet(); + return null; + }); + } + + private static String proxyRequest(final Handler handler) throws Exception { + return request(handler, "GET http://example.invalid/resource HTTP/1.1\r\n" + + "Host: example.invalid\r\nConnection: close\r\n\r\n"); + } + + private static void assertDirectPeerRequest(final String target, final boolean expected) + throws Exception { + final Handler check = new Handler.Abstract() { + @Override + public boolean handle(final Request request, final Response response, + final Callback callback) { + final boolean actual = Jetty12ProxyChain.ProxyPolicyHandler.isDirectPeerRequest( + request, "localpeer"); + response.setStatus(actual == expected ? 204 : 500); + callback.succeeded(); + return true; + } + }; + final String host = target.startsWith("http://") + ? java.net.URI.create(target).getRawAuthority() : "127.0.0.1:%d"; + final String response = request(check, "GET " + target + " HTTP/1.1\r\nHost: " + + host + "\r\nConnection: close\r\n\r\n"); + assertTrue(response, response.startsWith("HTTP/1.1 204")); + } + } + + /** {@code .yacy} authority rewrite in front of the proxy selection. */ + public static class DomainHandlerTest { + + @Test + public void rewritesPeerAuthorityBeforeCallingNextHandler() throws Exception { + final AtomicReference<String> authority = new AtomicReference<>(); + final AtomicReference<String> hostHeader = new AtomicReference<>(); + final Handler capture = new Handler.Abstract() { + @Override + public boolean handle(final Request request, final Response response, + final Callback callback) { + authority.set(request.getHttpURI().getAuthority()); + hostHeader.set(request.getHeaders().get(HttpHeader.HOST)); + response.setStatus(204); + callback.succeeded(); + return true; + } + }; + final Server server = new Server(); + final ServerConnector connector = new ServerConnector(server, 1, 1); + connector.setHost("127.0.0.1"); + connector.setPort(0); + server.addConnector(connector); + server.setHandler(new Jetty12ProxyChain.DomainHandler(capture, resolver(), ignored -> false)); + try { + server.start(); + try (Socket client = new Socket("127.0.0.1", connector.getLocalPort())) { + final OutputStream output = client.getOutputStream(); + output.write(("GET /status?q=1 HTTP/1.1\r\nHost: peer.yacy\r\nConnection: close\r\n\r\n") + .getBytes(StandardCharsets.US_ASCII)); + output.flush(); + try (InputStream input = client.getInputStream()) { + while (input.read() >= 0) { + // Drain the response so the exchange completes before assertions. + } + } + } + assertEquals("198.51.100.7:8090", authority.get()); + assertEquals("198.51.100.7:8090", hostHeader.get()); + } finally { + server.stop(); + server.join(); + } + } + + private static AlternativeDomainNames resolver() { + return new AlternativeDomainNames() { + @Override + public String resolve(final String name) { + return "peer.yacy".equals(name) ? "198.51.100.7:8090" : null; + } + + @Override public String myAlternativeAddress() { return "local.yacy"; } + @Override public Set<String> myIPs() { return Collections.singleton("127.0.0.1"); } + @Override public int myPort() { return 8090; } + @Override public String myName() { return "local"; } + @Override public String myID() { return "local-id"; } + }; + } + } + + /** Raw byte tunnelling for permitted CONNECT destinations. */ + public static class ConnectTunnelHandlerTest { + + @Test + public void tunnelsBytesToPermittedDestination() throws Exception { + try (ServerSocket origin = new ServerSocket(0, 1)) { + final Thread echo = new Thread(() -> echoOnce(origin), "connect-origin-echo"); + echo.start(); + final Server proxy = new Server(); + final ServerConnector connector = new ServerConnector(proxy, 1, 1); + connector.setHost("127.0.0.1"); + connector.setPort(0); + proxy.addConnector(connector); + proxy.setHandler(new Jetty12ProxyChain.ConnectTunnelHandler( + (Handler) null, (request, destination) -> null)); + try { + proxy.start(); + try (Socket client = new Socket("127.0.0.1", connector.getLocalPort())) { + final OutputStream output = client.getOutputStream(); + output.write(("CONNECT 127.0.0.1:" + origin.getLocalPort() + + " HTTP/1.1\r\nHost: 127.0.0.1:" + origin.getLocalPort() + + "\r\n\r\n").getBytes(StandardCharsets.US_ASCII)); + output.flush(); + final InputStream input = client.getInputStream(); + final String headers = readHeaders(input); + assertEquals(true, headers.startsWith("HTTP/1.1 200")); + output.write("tunnel-data".getBytes(StandardCharsets.US_ASCII)); + output.flush(); + assertEquals("tunnel-data", new String(input.readNBytes(11), StandardCharsets.US_ASCII)); + } + } finally { + proxy.stop(); + proxy.join(); + echo.join(5000L); + } + } + } + + private static void echoOnce(final ServerSocket origin) { + try (Socket socket = origin.accept()) { + final byte[] data = socket.getInputStream().readNBytes(11); + socket.getOutputStream().write(data); + socket.getOutputStream().flush(); + } catch (final Exception error) { + throw new AssertionError(error); + } + } + + private static String readHeaders(final InputStream input) throws Exception { + final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + int matched = 0; + while (matched < 4) { + final int next = input.read(); + if (next < 0) { + break; + } + bytes.write(next); + final byte expected = new byte[] {'\r', '\n', '\r', '\n'}[matched]; + matched = next == expected ? matched + 1 : (next == '\r' ? 1 : 0); + } + return bytes.toString(StandardCharsets.US_ASCII); + } + } + + /** Streaming forward proxy for absolute and transparent origin-form requests. */ + public static class ForwardProxyHandlerTest { + + @Test + public void forwardsAbsoluteHttpRequest() throws Exception { + assertForwarded(true); + } + + @Test + public void forwardsTransparentOriginFormRequest() throws Exception { + assertForwarded(false); + } + + private static void assertForwarded(final boolean absoluteTarget) throws Exception { + final ByteArrayOutputStream captured = new ByteArrayOutputStream(); + final CountDownLatch captureComplete = new CountDownLatch(1); + final HttpServer origin = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + origin.createContext("/resource", exchange -> { + final byte[] body = "proxied-content".getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, body.length); + exchange.getResponseBody().write(body); + exchange.close(); + }); + origin.start(); + final Server proxy = new Server(); + final ServerConnector connector = new ServerConnector(proxy, 1, 1); + connector.setHost("127.0.0.1"); + connector.setPort(0); + proxy.addConnector(connector); + proxy.setHandler(new Jetty12ProxyChain.ForwardProxyHandler( + (request, upstream) -> new Jetty12ProxyChain.ForwardProxyHandler.Capture() { + @Override + public void append(final ByteBuffer content) { + final byte[] bytes = new byte[content.remaining()]; + content.get(bytes); + captured.writeBytes(bytes); + } + + @Override + public void complete() { + captureComplete.countDown(); + } + })); + try { + proxy.start(); + try (Socket client = new Socket("127.0.0.1", connector.getLocalPort())) { + final OutputStream output = client.getOutputStream(); + final String target = absoluteTarget + ? "http://127.0.0.1:" + origin.getAddress().getPort() + "/resource" + : "/resource"; + output.write(("GET " + target + " HTTP/1.1\r\nHost: 127.0.0.1:" + + origin.getAddress().getPort() + "\r\nConnection: close\r\n\r\n") + .getBytes(StandardCharsets.US_ASCII)); + output.flush(); + final String response = readAll(client.getInputStream()); + assertTrue(response.startsWith("HTTP/1.1 200")); + assertTrue(response.contains("proxied-content")); + } + assertTrue(captureComplete.await(2, TimeUnit.SECONDS)); + assertArrayEquals("proxied-content".getBytes(StandardCharsets.UTF_8), + captured.toByteArray()); + } finally { + proxy.stop(); + proxy.join(); + origin.stop(0); + } + } + } + + /** Cache hits are served with 203 without a network round trip. */ + public static class ProxyCacheHandlerTest { + + @Test + public void servesFreshCachedResponseWithoutNetworkHandler() throws Exception { + final HttpFields headers = HttpFields.build().put("Content-Type", "text/plain").asImmutable(); + final Handler cache = new Jetty12ProxyChain.ProxyCacheHandler(request -> + new Jetty12ProxyChain.ProxyCacheHandler.CachedResponse(headers, + "cached-content".getBytes(StandardCharsets.UTF_8))); + final String response = request(cache, + "GET http://example.invalid/resource HTTP/1.1\r\n" + + "Host: example.invalid\r\nConnection: close\r\n\r\n"); + assertTrue(response, response.startsWith("HTTP/1.1 203")); + assertTrue(response, response.contains("cached-content")); + } + } + + /** Size-capped copy of streamed proxy responses for the cache/index path. */ + public static class ProxyResponseStoreTest { + + @Test + public void discardsCacheCaptureWhenStreamingResponseExceedsLimit() { + final Jetty12ProxyChain.ProxyResponseStore.CacheCapture capture = + new Jetty12ProxyChain.ProxyResponseStore.CacheCapture(null, null, 4); + + capture.append(ByteBuffer.wrap(new byte[] {1, 2})); + capture.append(ByteBuffer.wrap(new byte[] {3, 4})); + assertFalse(capture.isDiscarded()); + assertEquals(4, capture.bufferedSize()); + + capture.append(ByteBuffer.wrap(new byte[] {5})); + assertTrue(capture.isDiscarded()); + assertEquals(0, capture.bufferedSize()); + + capture.append(ByteBuffer.wrap(new byte[] {6, 7})); + assertEquals(0, capture.bufferedSize()); + capture.complete(); + } + } + + static String request(final Handler handler, final String requestTemplate) throws Exception { + final Server server = new Server(); + final ServerConnector connector = new ServerConnector(server, 1, 1); + connector.setHost("127.0.0.1"); + connector.setPort(0); + server.addConnector(connector); + server.setHandler(handler); + try { + server.start(); + try (Socket client = new Socket("127.0.0.1", connector.getLocalPort())) { + final OutputStream output = client.getOutputStream(); + output.write(String.format(requestTemplate, connector.getLocalPort()) + .getBytes(StandardCharsets.US_ASCII)); + output.flush(); + return readAll(client.getInputStream()); + } + } finally { + server.stop(); + server.join(); + } + } + + static String readAll(final InputStream input) throws Exception { + final ByteArrayOutputStream result = new ByteArrayOutputStream(); + input.transferTo(result); + return result.toString(StandardCharsets.ISO_8859_1); + } +} diff --git a/test/java/net/yacy/http/Jetty9LoggingFacadeTest.java b/test/java/net/yacy/http/Jetty9LoggingFacadeTest.java deleted file mode 100644 index bba2b6924..000000000 --- a/test/java/net/yacy/http/Jetty9LoggingFacadeTest.java +++ /dev/null @@ -1,16 +0,0 @@ -package net.yacy.http; - -import static org.junit.Assert.assertEquals; - -import org.eclipse.jetty.util.log.Log; -import org.junit.Test; - -/** Jetty 9 baseline only; replace this test when Jetty9HttpServerImpl is removed. */ -public class Jetty9LoggingFacadeTest { - - @Test - public void jetty9UsesItsSlf4jFacade() { - assertEquals("org.eclipse.jetty.util.log.Slf4jLog", - Log.getLogger("org.eclipse.jetty.yacy.logging.test").getClass().getName()); - } -} diff --git a/test/java/net/yacy/http/servlets/Jetty12QoSFilterTest.java b/test/java/net/yacy/http/servlets/Jetty12QoSFilterTest.java new file mode 100644 index 000000000..2920e44e2 --- /dev/null +++ b/test/java/net/yacy/http/servlets/Jetty12QoSFilterTest.java @@ -0,0 +1,34 @@ +package net.yacy.http.servlets; + +import static org.junit.Assert.assertEquals; + +import java.lang.reflect.Proxy; + +import javax.servlet.ServletRequest; + +import org.junit.Test; + +public class Jetty12QoSFilterTest { + + @Test + public void givesLocalhostServerNameHighestPriority() { + final ServletRequest request = (ServletRequest) Proxy.newProxyInstance( + ServletRequest.class.getClassLoader(), new Class<?>[] {ServletRequest.class}, + (proxy, method, arguments) -> "getServerName".equals(method.getName()) + ? "localhost" : defaultValue(method.getReturnType())); + assertEquals(10, new YaCyQoSFilter().getPriority(request)); + } + + private static Object defaultValue(final Class<?> type) { + if (!type.isPrimitive()) { + return null; + } + if (type == boolean.class) { + return false; + } + if (type == char.class) { + return '\0'; + } + return 0; + } +} diff --git a/test/java/net/yacy/http/servlets/ServletResourceTest.java b/test/java/net/yacy/http/servlets/ServletResourceTest.java new file mode 100644 index 000000000..bac0cd4ec --- /dev/null +++ b/test/java/net/yacy/http/servlets/ServletResourceTest.java @@ -0,0 +1,65 @@ +package net.yacy.http.servlets; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class ServletResourceTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void servesFileMetadataAndRangesWithoutJetty() throws Exception { + final Path root = this.temporaryFolder.newFolder("root").toPath(); + final Path file = Files.writeString(root.resolve("sample.txt"), "0123456789", StandardCharsets.UTF_8); + final ServletResource base = ServletResource.from(root.toString()); + final ServletResource resource = base.addPath("/sample.txt"); + + assertTrue(resource.exists()); + assertFalse(resource.isDirectory()); + assertEquals(10L, resource.length()); + assertEquals(file.toFile(), resource.getFile()); + assertTrue(resource.lastModified() > 0L); + + final ByteArrayOutputStream range = new ByteArrayOutputStream(); + resource.writeTo(range, 2L, 4L); + assertEquals("2345", range.toString(StandardCharsets.UTF_8)); + } + + @Test + public void rejectsPathsOutsideResourceBase() throws Exception { + final ServletResource base = ServletResource.from( + this.temporaryFolder.newFolder("root").toPath().toString()); + try { + base.addPath("../outside.txt"); + fail("Path traversal must be rejected"); + } catch (final IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("escapes its base")); + } + } + + @Test + public void createsEscapedDirectoryListing() throws Exception { + final Path root = this.temporaryFolder.newFolder("root").toPath(); + Files.writeString(root.resolve("<entry>.txt"), "content", StandardCharsets.UTF_8); + Files.createDirectory(root.resolve("child")); + final ServletResource directory = ServletResource.from(root.toUri().toURL()); + + final String listing = directory.getListHTML("/files/", true, null); + assertTrue(listing.contains("href=\"../\"")); + assertTrue(listing.contains("%3Centry%3E.txt")); + assertTrue(listing.contains("<entry>.txt")); + assertTrue(listing.contains("href=\"/files/child/\"")); + } +} |
