diff options
| author | Michael Peter Christen <mc@yacy.net> | 2026-07-11 23:40:15 +0200 |
|---|---|---|
| committer | Michael Peter Christen <mc@yacy.net> | 2026-07-11 23:40:15 +0200 |
| commit | bfddfe5e7ed90a80b4acf9a71a567b61b77cfcc4 (patch) | |
| tree | d1918d46d198606e867b9d83e0a64bff19c0891b /test | |
| parent | edeb6162c435e4ad9098233eed4f8c9c9b0312e2 (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.java | 37 | ||||
| -rw-r--r-- | test/java/net/yacy/http/InetPathAccessHandlerTest.java | 12 | ||||
| -rwxr-xr-x | test/jetty-solr-dependency-guard.sh | 81 | ||||
| -rw-r--r-- | test/jetty/RelocateJettyPackages.java | 135 | ||||
| -rwxr-xr-x | test/solr9-jetty-bridge-spike.sh | 39 |
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." |
