summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorMichael Peter Christen <mc@yacy.net>2026-07-11 23:40:15 +0200
committerMichael Peter Christen <mc@yacy.net>2026-07-11 23:40:15 +0200
commitbfddfe5e7ed90a80b4acf9a71a567b61b77cfcc4 (patch)
treed1918d46d198606e867b9d83e0a64bff19c0891b /test
parentedeb6162c435e4ad9098233eed4f8c9c9b0312e2 (diff)
isolate Solr 9.0 Jetty client dependencies in a relocated bridge
Diffstat (limited to 'test')
-rw-r--r--test/java/net/yacy/http/AdminSecurityTest.java37
-rw-r--r--test/java/net/yacy/http/InetPathAccessHandlerTest.java12
-rwxr-xr-xtest/jetty-solr-dependency-guard.sh81
-rw-r--r--test/jetty/RelocateJettyPackages.java135
-rwxr-xr-xtest/solr9-jetty-bridge-spike.sh39
5 files changed, 304 insertions, 0 deletions
diff --git a/test/java/net/yacy/http/AdminSecurityTest.java b/test/java/net/yacy/http/AdminSecurityTest.java
index 4fd8abb58..196eae3ea 100644
--- a/test/java/net/yacy/http/AdminSecurityTest.java
+++ b/test/java/net/yacy/http/AdminSecurityTest.java
@@ -106,4 +106,41 @@ public class AdminSecurityTest {
// a request from localhost referred by a remote page is not a localhost access
Assert.assertFalse(AdminSecurity.isLocalhostAccess("127.0.0.1", "example.org"));
}
+
+ /** Test the complete request-level policy used by the container adapter. */
+ @Test
+ public void testAdminAccessPolicy() {
+ final String user = "admin";
+ final String hash = AdminSecurity.calcHash(user + ":secret");
+ final AdminAccessPolicy localAllowed = new AdminAccessPolicy(
+ false, false, true, true, user, hash);
+
+ Assert.assertEquals(AdminAccessPolicy.Decision.PUBLIC,
+ localAllowed.decide("/index.html", "192.0.2.1", null, null));
+ Assert.assertEquals(AdminAccessPolicy.Decision.ADMIN_REQUIRED,
+ localAllowed.decide("/Settings_p.html", "192.0.2.1", null, null));
+ Assert.assertEquals(AdminAccessPolicy.Decision.LOCAL_BYPASS,
+ localAllowed.decide("/Settings_p.html", "127.0.0.1", null, null));
+ Assert.assertEquals(AdminAccessPolicy.Decision.ADMIN_REQUIRED,
+ localAllowed.decide("/Settings_p.html", "127.0.0.1", "https://example.org/", null));
+
+ final AdminAccessPolicy loginRequired = new AdminAccessPolicy(
+ false, false, true, false, user, hash);
+ Assert.assertEquals(AdminAccessPolicy.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,
+ 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());
+ }
}
diff --git a/test/java/net/yacy/http/InetPathAccessHandlerTest.java b/test/java/net/yacy/http/InetPathAccessHandlerTest.java
index 5dfdf8a4f..641ad8bf1 100644
--- a/test/java/net/yacy/http/InetPathAccessHandlerTest.java
+++ b/test/java/net/yacy/http/InetPathAccessHandlerTest.java
@@ -32,6 +32,18 @@ import org.junit.Test;
*/
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.
*
diff --git a/test/jetty-solr-dependency-guard.sh b/test/jetty-solr-dependency-guard.sh
new file mode 100755
index 000000000..0f2fad8a2
--- /dev/null
+++ b/test/jetty-solr-dependency-guard.sh
@@ -0,0 +1,81 @@
+#!/usr/bin/env sh
+
+# Guard the classpath boundary needed to migrate YaCy's embedded server from
+# Jetty 9 to Jetty 12 while Solr 9's Jetty client is relocated into a private
+# package and kept out of YaCy source code.
+
+set -eu
+
+repo_root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
+cd "$repo_root"
+
+fail() {
+ echo "FAIL: $*" >&2
+ exit 1
+}
+
+if grep -R -n -E \
+ --include='*.java' \
+ '(Http2SolrClient|CloudHttp2SolrClient|ConcurrentUpdateHttp2SolrClient|LBHttp2SolrClient|JettySolrRunner)' \
+ source >/dev/null 2>&1; then
+ grep -R -n -E \
+ --include='*.java' \
+ '(Http2SolrClient|CloudHttp2SolrClient|ConcurrentUpdateHttp2SolrClient|LBHttp2SolrClient|JettySolrRunner)' \
+ source >&2
+ fail "YaCy source must not use Solr's Jetty-backed clients or runner"
+fi
+
+if grep -E \
+ 'name="(jetty-deploy|jetty-jmx)"' \
+ ivy.xml >/dev/null 2>&1; then
+ fail "ivy.xml contains a forbidden direct Jetty dependency"
+fi
+
+grep -E 'name="jetty-client".*conf="solr9-bridge->master"' ivy.xml >/dev/null 2>&1 || \
+ fail "jetty-client must only be a direct input of the Solr 9 bridge"
+
+grep -E 'org="org.eclipse.jetty" name="jetty-io"' ivy.xml >/dev/null 2>&1 || \
+ fail "jetty-io must be an explicit dependency because YaCy imports its API"
+
+for artifact in http2-client http2-common http2-http-client-transport; do
+ grep -E "org=\"org.eclipse.jetty.http2\" name=\"$artifact\".*conf=\"solr9-bridge->master\"" ivy.xml >/dev/null 2>&1 || \
+ fail "$artifact must only be an input of the Solr 9 bridge"
+done
+
+if [ -d lib ]; then
+ for pattern in \
+ 'jetty-deploy-*.jar' \
+ 'jetty-jmx-*.jar' \
+ 'solr-core-*.jar' \
+ 'solr-solrj-*.jar' \
+ 'solr-scripting-*.jar' \
+ 'http2-*.jar'; do
+ for artifact in lib/$pattern; do
+ [ -e "$artifact" ] || continue
+ fail "forbidden resolved artifact: $artifact"
+ done
+ done
+
+ for artifact in \
+ solr-core-9.0.0 \
+ solr-solrj-9.0.0 \
+ solr-scripting-9.0.0 \
+ jetty-client-9.4.58.v20250814 \
+ jetty-http-9.4.58.v20250814 \
+ jetty-io-9.4.58.v20250814 \
+ jetty-util-9.4.58.v20250814 \
+ http2-client-9.4.58.v20250814 \
+ http2-common-9.4.58.v20250814 \
+ http2-http-client-transport-9.4.58.v20250814; do
+ jar="lib/solr9-bridge-$artifact.jar"
+ [ -f "$jar" ] || fail "missing generated bridge artifact: $jar"
+ if jar tf "$jar" | grep '^org/eclipse/jetty/' >/dev/null 2>&1; then
+ fail "unrelocated Jetty class in $jar"
+ fi
+ if zipgrep -a -E 'org(/|\.)eclipse(/|\.)jetty' "$jar" >/dev/null 2>&1; then
+ fail "unrelocated Jetty reference in $jar"
+ fi
+ done
+fi
+
+echo "PASS: Solr 9 uses only the relocated Jetty client island."
diff --git a/test/jetty/RelocateJettyPackages.java b/test/jetty/RelocateJettyPackages.java
new file mode 100644
index 000000000..0766388e5
--- /dev/null
+++ b/test/jetty/RelocateJettyPackages.java
@@ -0,0 +1,135 @@
+/*
+ * Proof-of-concept bytecode relocator for the Jetty 12 migration.
+ *
+ * This is deliberately a small build tool, not YaCy runtime code. It keeps
+ * Solr's public packages unchanged while moving its private Jetty 9 linkage
+ * below net.yacy.solr9.jetty.
+ */
+package net.yacy.test.jetty;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Enumeration;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.JarOutputStream;
+
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.commons.ClassRemapper;
+import org.objectweb.asm.commons.Remapper;
+
+public final class RelocateJettyPackages {
+
+ private static final String SOURCE_INTERNAL = "org/eclipse/jetty";
+ private static final String TARGET_INTERNAL = "net/yacy/solr9/jetty";
+ private static final String SOURCE_BINARY = "org.eclipse.jetty";
+ private static final String TARGET_BINARY = "net.yacy.solr9.jetty";
+
+ private RelocateJettyPackages() {
+ }
+
+ public static void main(final String[] args) throws IOException {
+ if (args.length != 2) {
+ throw new IllegalArgumentException("usage: RelocateJettyPackages INPUT.jar OUTPUT.jar");
+ }
+ relocate(Path.of(args[0]), Path.of(args[1]));
+ }
+
+ private static void relocate(final Path input, final Path output) throws IOException {
+ Files.createDirectories(output.toAbsolutePath().getParent());
+ final Remapper remapper = new JettyRemapper();
+
+ try (JarFile source = new JarFile(input.toFile());
+ JarOutputStream target = new JarOutputStream(Files.newOutputStream(output))) {
+ final Enumeration<JarEntry> entries = source.entries();
+ while (entries.hasMoreElements()) {
+ final JarEntry entry = entries.nextElement();
+ if (entry.isDirectory() || isSignature(entry.getName())) {
+ continue;
+ }
+ final String outputName = relocateEntryName(entry.getName());
+ final JarEntry outputEntry = new JarEntry(outputName);
+ outputEntry.setTime(entry.getTime());
+ target.putNextEntry(outputEntry);
+ try (InputStream stream = source.getInputStream(entry)) {
+ final byte[] content = readAll(stream);
+ if (entry.getName().endsWith(".class")) {
+ target.write(relocateClass(content, remapper));
+ } else if (entry.getName().startsWith("META-INF/services/")) {
+ target.write(new String(content, StandardCharsets.UTF_8)
+ .replace(SOURCE_BINARY, TARGET_BINARY)
+ .getBytes(StandardCharsets.UTF_8));
+ } else {
+ target.write(content);
+ }
+ }
+ target.closeEntry();
+ }
+ }
+ }
+
+ private static final class JettyRemapper extends Remapper {
+
+ private JettyRemapper() {
+ super(Opcodes.ASM9);
+ }
+
+ @Override
+ public String map(final String internalName) {
+ return replacePrefix(internalName, SOURCE_INTERNAL, TARGET_INTERNAL);
+ }
+
+ @Override
+ public Object mapValue(final Object value) {
+ if (value instanceof String) {
+ final String text = (String) value;
+ return text.replace(SOURCE_BINARY, TARGET_BINARY)
+ .replace(SOURCE_INTERNAL, TARGET_INTERNAL);
+ }
+ return super.mapValue(value);
+ }
+ }
+
+ private static byte[] relocateClass(final byte[] content, final Remapper remapper) {
+ final ClassReader reader = new ClassReader(content);
+ final ClassWriter writer = new ClassWriter(0);
+ reader.accept(new ClassRemapper(writer, remapper), 0);
+ return writer.toByteArray();
+ }
+
+ private static boolean isSignature(final String name) {
+ final String upper = name.toUpperCase(java.util.Locale.ROOT);
+ return upper.startsWith("META-INF/")
+ && (upper.endsWith(".SF") || upper.endsWith(".RSA") || upper.endsWith(".DSA"));
+ }
+
+ private static String replacePrefix(final String value, final String source, final String target) {
+ if (value.equals(source)) {
+ return target;
+ }
+ if (value.startsWith(source + "/")) {
+ return target + value.substring(source.length());
+ }
+ return value;
+ }
+
+ private static String relocateEntryName(final String name) {
+ final String internalName = replacePrefix(name, SOURCE_INTERNAL, TARGET_INTERNAL);
+ if (internalName.startsWith("META-INF/services/")) {
+ return internalName.replace(SOURCE_BINARY, TARGET_BINARY);
+ }
+ return internalName;
+ }
+
+ private static byte[] readAll(final InputStream stream) throws IOException {
+ final ByteArrayOutputStream output = new ByteArrayOutputStream();
+ stream.transferTo(output);
+ return output.toByteArray();
+ }
+}
diff --git a/test/solr9-jetty-bridge-spike.sh b/test/solr9-jetty-bridge-spike.sh
new file mode 100755
index 000000000..00eb0ff5c
--- /dev/null
+++ b/test/solr9-jetty-bridge-spike.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env sh
+
+# Exercise the generated Solr 9.0 bridge. Run `ant compile` first. An optional
+# JETTY12_CLASSPATH verifies coexistence with already resolved Jetty 12 jars.
+
+set -eu
+
+repo_root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
+cd "$repo_root"
+
+fail() {
+ echo "FAIL: $*" >&2
+ exit 1
+}
+
+[ -d build/classes/java/main ] || fail "missing compiled YaCy classes; run ant compile first"
+test/jetty-solr-dependency-guard.sh
+
+work=$(mktemp -d "${TMPDIR:-/tmp}/yacy-solr9-bridge-test.XXXXXX")
+trap 'rm -rf "$work"' EXIT HUP INT TERM
+
+classpath=build/classes/java/main
+for archive in lib/*.jar libt/*.jar; do
+ case $(basename "$archive") in
+ yacycore.jar) continue ;;
+ esac
+ classpath="$classpath:$archive"
+done
+if [ -n "${JETTY12_CLASSPATH:-}" ]; then
+ classpath="$classpath:$JETTY12_CLASSPATH"
+fi
+
+javac --release 17 -cp "$classpath" -d "$work" \
+ test/java/net/yacy/cora/federate/solr/connector/EmbeddedSolrConnectorTest.java
+
+java -cp "$work:$classpath" org.junit.runner.JUnitCore \
+ net.yacy.cora.federate.solr.connector.EmbeddedSolrConnectorTest
+
+echo "PASS: integrated Solr 9.0 bridge starts, updates, queries and closes."