summaryrefslogtreecommitdiff
path: root/test/java/net
diff options
context:
space:
mode:
authorMichael Peter Christen <mc@yacy.net>2026-07-12 12:03:57 +0200
committerMichael Peter Christen <mc@yacy.net>2026-07-12 12:03:57 +0200
commit554911a88f8c5084146a8a098965f18e67416525 (patch)
tree55012e1b4002e42899b02f51e290925566ae74c8 /test/java/net
parentb2e9142ea9a61af8c6e55a39a392136306dc0312 (diff)
Migration to Jetty 12.1.11
Diffstat (limited to 'test/java/net')
-rw-r--r--test/java/net/yacy/http/AdminSecurityTest.java28
-rw-r--r--test/java/net/yacy/http/HttpServerBootstrapConfigTest.java2
-rw-r--r--test/java/net/yacy/http/InetPathAccessHandlerTest.java355
-rw-r--r--test/java/net/yacy/http/Jetty12HttpServerTest.java427
-rw-r--r--test/java/net/yacy/http/Jetty12ProxyChainTest.java457
-rw-r--r--test/java/net/yacy/http/Jetty9LoggingFacadeTest.java16
-rw-r--r--test/java/net/yacy/http/servlets/Jetty12QoSFilterTest.java34
-rw-r--r--test/java/net/yacy/http/servlets/ServletResourceTest.java65
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("&lt;entry&gt;.txt"));
+ assertTrue(listing.contains("href=\"/files/child/\""));
+ }
+}