towards a Jetty 9 decoupling baseline for the Jetty 12 migration

This commit is contained in:
Michael Peter Christen committed 2026-07-12 00:49:49 +02:00
1 parent bfddfe5e7e
commit b2e9142ea9
33 files changed
+1077 -130

No files matched your search

+2
View File
@@ -22,5 +22,7 @@
<classpathentry kind="lib" path="lib/solr9-bridge-http2-client-9.4.58.v20250814.jar"/> <classpathentry kind="lib" path="lib/solr9-bridge-http2-client-9.4.58.v20250814.jar"/>
<classpathentry kind="lib" path="lib/solr9-bridge-http2-common-9.4.58.v20250814.jar"/> <classpathentry kind="lib" path="lib/solr9-bridge-http2-common-9.4.58.v20250814.jar"/>
<classpathentry kind="lib" path="lib/solr9-bridge-http2-http-client-transport-9.4.58.v20250814.jar"/> <classpathentry kind="lib" path="lib/solr9-bridge-http2-http-client-transport-9.4.58.v20250814.jar"/>
<classpathentry kind="lib" path="lib/solr9-bridge-slf4j-api-1.7.36.jar"/>
<classpathentry kind="lib" path="lib/solr9-bridge-slf4j-jdk14-1.7.36.jar"/>
<classpathentry kind="output" path="gen"/> <classpathentry kind="output" path="gen"/>
</classpath> </classpath>
+8 -4
View File
@@ -1,10 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.eclipse.ant.AntBuilderLaunchConfigurationType"> <launchConfiguration type="org.eclipse.ant.AntBuilderLaunchConfigurationType">
<booleanAttribute key="org.eclipse.ant.ui.ATTR_TARGETS_UPDATED" value="true"/>
<stringAttribute key="org.eclipse.ant.ui.ATTR_ANT_FULL_TARGETS" value="build-solr9-bridge,"/> <stringAttribute key="org.eclipse.ant.ui.ATTR_ANT_FULL_TARGETS" value="build-solr9-bridge,"/>
<stringAttribute key="org.eclipse.ant.ui.ATTR_ANT_MANUAL_TARGETS" value="build-solr9-bridge,"/> <stringAttribute key="org.eclipse.ant.ui.ATTR_ANT_MANUAL_TARGETS" value="build-solr9-bridge,"/>
<booleanAttribute key="org.eclipse.ui.externaltools.ATTR_TRIGGERS_CONFIGURED" value="true"/> <booleanAttribute key="org.eclipse.ant.ui.ATTR_TARGETS_UPDATED" value="true"/>
<booleanAttribute key="org.eclipse.debug.ui.ATTR_LAUNCH_IN_BACKGROUND" value="false"/>
<stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="org.eclipse.ant.ui.AntClasspathProvider"/>
<booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="true"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${project_loc}/build.xml"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_REFRESH_SCOPE" value="${project}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_RUN_BUILD_KINDS" value="full,"/> <stringAttribute key="org.eclipse.ui.externaltools.ATTR_RUN_BUILD_KINDS" value="full,"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:/yacy/build.xml}"/> <booleanAttribute key="org.eclipse.ui.externaltools.ATTR_TRIGGERS_CONFIGURED" value="true"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_WORKING_DIRECTORY" value="${workspace_loc:/yacy}"/> <stringAttribute key="org.eclipse.ui.externaltools.ATTR_WORKING_DIRECTORY" value="${project_loc}"/>
</launchConfiguration> </launchConfiguration>
+130
View File
@@ -167,6 +167,51 @@ Jetty version on the resolved classpath and the private Solr island:
6. Proxy traffic including CONNECT 6. Proxy traffic including CONNECT
7. `test/jetty-smoke-test.sh` 7. `test/jetty-smoke-test.sh`
The live gates are split by the environment they require:
- `test/jetty-smoke-test.sh` checks HTTP methods, ranges, conditional requests,
error dispatch, and gzip request/response handling against a running peer;
- `test/jetty-auth-smoke-test.sh` checks localhost, `bin/apicall.sh`, optional
credentials, and an optional real non-loopback path;
- `test/jetty-peer-start-smoke-test.sh` starts and stops an isolated peer through
explicit harness commands and queries its embedded Solr core;
- `test/remote-solr-smoke-test.sh` queries an explicitly configured external
Solr instance through YaCy's Apache-HttpClient-backed `RemoteInstance`;
- `test/proxy-smoke-test.sh` checks HTTP proxy traffic and an HTTPS CONNECT
tunnel against explicitly configured controlled targets.
The environment-dependent gates exit with status 2 when their required target
or isolated-peer harness has not been supplied. This is a reported skip, not a
successful verification.
The final migration acceptance must run the authentication gate with no skips:
```sh
YACY_SMOKE_REQUIRE_COMPLETE=true \
YACY_SMOKE_ADMIN_USER=admin \
YACY_SMOKE_ADMIN_PASSWORD='the configured password' \
YACY_SMOKE_REMOTE_BASE_URL='http://a-real-non-loopback-peer-address:8090' \
test/jetty-auth-smoke-test.sh
```
The HTTP range contract includes a single satisfiable range (`206`), multiple
satisfiable ranges as `multipart/byteranges`, and an unsatisfiable range
(`416`).
### Switch-time logging tests
`Slf4jJulBridgeTest` is version-neutral and must pass both before and after the
server switch. It proves that the public SLF4J 2 provider routes the
`org.eclipse.jetty` logger namespace into `java.util.logging` and therefore the
YaCy logging configuration.
`Jetty9LoggingFacadeTest` is deliberately a Jetty 9 baseline test. It imports
Jetty 9's removed `org.eclipse.jetty.util.log.Log` API and asserts the old
`Slf4jLog` facade. Remove it together with `Jetty9HttpServerImpl` during the
switch and replace it with a Jetty 12 integration test that starts and stops a
real server while capturing an `org.eclipse.jetty` record through JUL. The
Jetty 12 test must not assert an internal logger implementation class.
The following implementation phase may then replace `Jetty9HttpServerImpl` and The following implementation phase may then replace `Jetty9HttpServerImpl` and
the remaining Jetty adapter APIs without changing the Solr dependency graph. the remaining Jetty adapter APIs without changing the Solr dependency graph.
@@ -201,3 +246,88 @@ The portable address/path syntax of `serverClient` is represented by
`InetPathAccessRule`. `InetPathAccessHandler` remains the Jetty 9 matcher `InetPathAccessRule`. `InetPathAccessHandler` remains the Jetty 9 matcher
adapter; Jetty 12 can consume the normalized `address|path` rules with its adapter; Jetty 12 can consume the normalized `address|path` rules with its
native path-aware access handler. native path-aware access handler.
## P2.3 Handler Boundaries
Proxy request processing and cache processing no longer receive Jetty's
`Request`. `RequestCompletion` is the container-neutral signal that processing
is complete; `AbstractRemoteHandler` adapts it to Jetty 9's
`Request.setHandled(true)`. Consequently `ProxyHandler` and
`ProxyCacheHandler` have no Jetty imports.
The `proxyClient` regular-expression list is evaluated by the pure
`ProxyAccessPolicy`. The local virtual-host cache used by proxy detection is a
concurrent set because it is populated by both the discovery thread and
request threads.
The remaining Jetty handler classes now have explicit migration roles:
| Jetty 9 adapter | Responsibility to reproduce with Jetty 12 |
| --- | --- |
| `AbstractRemoteHandler` | detect proxy traffic and delegate CONNECT tunnelling |
| `CrashProtectionHandler` | outer exception barrier around proxy and servlet handlers |
| `YacyDomainHandler` | rewrite `.yacy` destinations and redispatch into the proxy chain |
| `YaCyErrorHandler` | render the container error page |
| `YaCyQoSFilter` | optional request prioritization when enabled in `web.xml` |
These classes intentionally remain container adapters. They must be ported
against the corresponding Jetty 12 APIs rather than replaced with servlet-only
approximations that would change CONNECT, error dispatch, or prioritization.
## P2.4 Embedded Server Bootstrap Contract
`HttpServerBootstrapConfig` is the common immutable input for Jetty 9 and the
future Jetty 12 implementation. It fixes the following startup values:
| Concern | Contract |
| --- | --- |
| HTTP binding | constructor host and port |
| Acceptor threads | half the available processors, clamped to 1 through 4 |
| Request header limit | 16,384 bytes |
| Connector idle timeout | 9,000 ms |
| HTTP accept queue | 128 |
| HTTPS | `server.https`, configured SSL port, initialized SSL context only |
| Web root | configured `htRootPath` below the application directory |
| Descriptors | `defaults/web.xml`, optionally `DATA/SETTINGS/web.xml` |
| Request decompression | Gzip inflate buffer of 4,096 bytes |
| Response compression | controlled by `server.response.compress.gzip` |
| Form limit | unlimited at the proxy-handler context boundary |
| Proxy handlers | present only when transparent proxy is enabled |
| Network access | configured `serverClient` address/path rules plus loopback |
| Authentication realm | configured administrator realm, unchanged for DIGEST hashes |
TLS preparation remains a YaCy bootstrap responsibility because it may import
a configured PKCS#12 file, create/update the JKS file, clear the one-shot
import settings, and construct the JDK `SSLContext`. The container adapter only
attaches that context to its HTTPS connector.
The request pipeline order is a behavioral requirement:
1. optional server-client address/path gate;
2. outer crash-protection barrier;
3. `.yacy` domain rewrite;
4. cached proxy response, when transparent proxy is enabled;
5. live HTTP proxy and CONNECT tunnel, when enabled;
6. root web application with monitor filter, admin security, gzip/inflate, and
`YaCyDefaultServlet`;
7. container default handler for requests left unhandled.
The connection-close listener must remove the matching `ConnectionInfo` entry
created by `MonitorFilter`. The default servlet and monitor filter remain
hard-coded mandatory components; additional servlet mappings come from the
merged web descriptors.
`YaCyHttpServer` defines the runtime contract used outside the adapter:
- synchronous start;
- synchronous stop followed by join;
- asynchronous delayed port reconnect without rebuilding the handler graph;
- HTTPS availability and bound-port reporting;
- administrator identity eviction/reload after credential changes;
- container version reporting;
- current non-idle worker-thread count.
A Jetty 12 implementation must first be added beside `Jetty9HttpServerImpl`
and satisfy this complete contract before the construction site in `yacy.java`
is switched. No caller outside the HTTP package should need a Jetty type or a
Jetty-version condition.
+21 -3
View File
@@ -38,6 +38,7 @@
<property name="lib" location="lib"/> <property name="lib" location="lib"/>
<property name="libt" location="libt"/> <property name="libt" location="libt"/>
<property name="build" location="build/classes/java/main"/> <!-- reuse Gradle build path --> <property name="build" location="build/classes/java/main"/> <!-- reuse Gradle build path -->
<property name="ivy.compile.stage" location="build/ivy-retrieve/compile"/>
<property name="solr9.bridge.build" location="build/solr9-bridge"/> <property name="solr9.bridge.build" location="build/solr9-bridge"/>
<property name="solr9.bridge.input" location="${solr9.bridge.build}/input"/> <property name="solr9.bridge.input" location="${solr9.bridge.build}/input"/>
<property name="solr9.bridge.tool" location="${solr9.bridge.build}/tool"/> <property name="solr9.bridge.tool" location="${solr9.bridge.build}/tool"/>
@@ -93,7 +94,23 @@
We can move there in a following step. We can move there in a following step.
${lib} just happens to be equal to ${ivy.lib.dir}. ${lib} just happens to be equal to ${ivy.lib.dir}.
--> -->
<ivy:retrieve conf="compile" pathid="compile.path" pattern="${ivy.lib.dir}/[artifact]-[revision].[ext]" /> <!--
Ivy retrieve does not remove stale revisions or replaced coordinates.
Resolve the complete public classpath into staging before touching lib/;
a failed resolve therefore leaves the last usable runtime intact. Only
after staging succeeds are stale public Jetty/Servlet jars removed and
the resolved classpath published. Private Solr jars use the distinct
solr9-bridge-* prefix and are rebuilt by build-solr9-bridge.
-->
<delete dir="${ivy.compile.stage}" failonerror="false" />
<mkdir dir="${ivy.compile.stage}" />
<ivy:retrieve conf="compile" pathid="compile.path" pattern="${ivy.compile.stage}/[artifact]-[revision].[ext]" />
<delete failonerror="false">
<fileset dir="${lib}" includes="javax.servlet-api-*.jar,jetty-*.jar" />
</delete>
<copy todir="${ivy.lib.dir}" overwrite="true">
<fileset dir="${ivy.compile.stage}" includes="**/*" />
</copy>
<ivy:retrieve conf="test" pathid="test.path" pattern="${libt}/[artifact]-[revision].[ext]" /> <ivy:retrieve conf="test" pathid="test.path" pattern="${libt}/[artifact]-[revision].[ext]" />
<ivy:retrieve conf="solr9-bridge" pathid="solr9.bridge.input.path" pattern="${solr9.bridge.input}/[artifact]-[revision].[ext]" /> <ivy:retrieve conf="solr9-bridge" pathid="solr9.bridge.input.path" pattern="${solr9.bridge.input}/[artifact]-[revision].[ext]" />
<ivy:retrieve conf="solr9-bridge-tool" pathid="solr9.bridge.tool.path" pattern="${solr9.bridge.tool.libs}/[artifact]-[revision].[ext]" /> <ivy:retrieve conf="solr9-bridge-tool" pathid="solr9.bridge.tool.path" pattern="${solr9.bridge.tool.libs}/[artifact]-[revision].[ext]" />
@@ -180,10 +197,10 @@
<fileset dir="${lib}" includes="solr9-bridge-*.jar" /> <fileset dir="${lib}" includes="solr9-bridge-*.jar" />
</path> </path>
<target name="build-solr9-bridge" depends="init,resolve" description="build the private Solr 9.0 Jetty client island"> <target name="build-solr9-bridge" depends="init,resolve" description="build the private Solr 9.0 Jetty and SLF4J 1.7 island">
<delete dir="${solr9.bridge.tool}" failonerror="false"/> <delete dir="${solr9.bridge.tool}" failonerror="false"/>
<delete failonerror="false"> <delete failonerror="false">
<fileset dir="${lib}" includes="solr9-bridge-*.jar,solr-core-9.0.0.jar,solr-solrj-9.0.0.jar,solr-scripting-9.0.0.jar,http2-*.jar" /> <fileset dir="${lib}" includes="solr9-bridge-*.jar,solr-core-9.0.0.jar,solr-solrj-9.0.0.jar,solr-scripting-9.0.0.jar,http2-*.jar,slf4j-*-1.7.36.jar" />
</delete> </delete>
<mkdir dir="${solr9.bridge.tool}"/> <mkdir dir="${solr9.bridge.tool}"/>
<javac srcdir="test/jetty" destdir="${solr9.bridge.tool}" <javac srcdir="test/jetty" destdir="${solr9.bridge.tool}"
@@ -450,6 +467,7 @@
<fileset dir="." includes="TEST-*" /> <fileset dir="." includes="TEST-*" />
</delete> </delete>
<delete dir="test/DATA" failonerror="false"/> <delete dir="test/DATA" failonerror="false"/>
<delete dir="${ivy.compile.stage}" failonerror="false"/>
<delete dir="${solr9.bridge.build}" failonerror="false"/> <delete dir="${solr9.bridge.build}" failonerror="false"/>
</target> </target>
+20 -6
View File
@@ -31,7 +31,12 @@
<dependency org="io.dropwizard.metrics" name="metrics-jmx" rev="4.2.39" conf="compile->master"/> <dependency org="io.dropwizard.metrics" name="metrics-jmx" rev="4.2.39" conf="compile->master"/>
<dependency org="io.opentracing" name="opentracing-noop" rev="0.33.0"/> <dependency org="io.opentracing" name="opentracing-noop" rev="0.33.0"/>
<dependency org="io.opentracing" name="opentracing-util" rev="0.33.0"/> <dependency org="io.opentracing" name="opentracing-util" rev="0.33.0"/>
<dependency org="javax.servlet" name="javax.servlet-api" rev="4.0.1"/> <!--
Jetty's EE8-compatible Servlet 4 API also contains the web.xml schemas
used by WebAppContext. Keep it as the single public javax.servlet API
before and after the Jetty 12 migration.
-->
<dependency org="org.eclipse.jetty.toolchain" name="jetty-servlet-api" rev="4.0.9" conf="compile->master"/>
<dependency org="javainetlocator" name="inetaddresslocator" rev="2.18" /> <dependency org="javainetlocator" name="inetaddresslocator" rev="2.18" />
<dependency org="eu.agno3.jcifs" name="jcifs-ng" rev="2.1.10" /> <dependency org="eu.agno3.jcifs" name="jcifs-ng" rev="2.1.10" />
<dependency org="net.arnx" name="jsonic" rev="1.3.10"/> <dependency org="net.arnx" name="jsonic" rev="1.3.10"/>
@@ -115,11 +120,18 @@
</dependency> </dependency>
<dependency org="org.locationtech.spatial4j" name="spatial4j" rev="0.8"/> <dependency org="org.locationtech.spatial4j" name="spatial4j" rev="0.8"/>
<dependency org="org.rrd4j" name="rrd4j" rev="3.10"/> <dependency org="org.rrd4j" name="rrd4j" rev="3.10"/>
<!-- Solr 9.0.0 directly references the SLF4J 1.7 StaticLoggerBinder API. --> <!--
<dependency org="org.slf4j" name="slf4j-api" rev="1.7.36" force="true"/> YaCy and the public Jetty runtime use SLF4J 2 with JUL as their single
<dependency org="org.slf4j" name="jcl-over-slf4j" rev="1.7.36"/> provider. Solr 9.0.0 directly references the removed SLF4J 1.7
<dependency org="org.slf4j" name="log4j-over-slf4j" rev="1.7.36"/> StaticLoggerBinder API, so its API and JUL binding are relocated into
<dependency org="org.slf4j" name="slf4j-jdk14" rev="1.7.36"/> the private Solr bridge instead of being exposed on this classpath.
-->
<dependency org="org.slf4j" name="slf4j-api" rev="2.0.17" conf="compile->master" force="true"/>
<dependency org="org.slf4j" name="jcl-over-slf4j" rev="2.0.17" conf="compile->master"/>
<dependency org="org.slf4j" name="log4j-over-slf4j" rev="2.0.17" conf="compile->master"/>
<dependency org="org.slf4j" name="slf4j-jdk14" rev="2.0.17" conf="compile->master"/>
<dependency org="org.slf4j" name="slf4j-api" rev="1.7.36" conf="solr9-bridge->master"/>
<dependency org="org.slf4j" name="slf4j-jdk14" rev="1.7.36" conf="solr9-bridge->master"/>
<!-- This does not match langdetect.jar from pre-ivy --> <!-- This does not match langdetect.jar from pre-ivy -->
<dependency org="org.tukaani" name="xz" rev="1.12"/> <dependency org="org.tukaani" name="xz" rev="1.12"/>
<dependency org="oro" name="oro" rev="2.0.8"/> <dependency org="oro" name="oro" rev="2.0.8"/>
@@ -131,5 +143,7 @@
<dependency org="org.ow2.asm" name="asm-commons" rev="9.9" conf="solr9-bridge-tool->master"/> <dependency org="org.ow2.asm" name="asm-commons" rev="9.9" conf="solr9-bridge-tool->master"/>
<dependency org="com.github.luben" name="zstd-jni" rev="1.5.7-11"></dependency> <dependency org="com.github.luben" name="zstd-jni" rev="1.5.7-11"></dependency>
<dependency org="ai.djl" name ="api" rev="0.36.0"></dependency> <dependency org="ai.djl" name ="api" rev="0.36.0"></dependency>
<!-- Different coordinates prevent Ivy from evicting duplicate Servlet API jars. -->
<exclude org="javax.servlet" module="javax.servlet-api"/>
</dependencies> </dependencies>
</ivy-module> </ivy-module>
@@ -26,10 +26,9 @@ package net.yacy.http;
import java.io.IOException; import java.io.IOException;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.HashSet;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
import java.util.StringTokenizer; import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@@ -42,17 +41,16 @@ import net.yacy.search.Switchboard;
import net.yacy.search.SwitchboardConstants; import net.yacy.search.SwitchboardConstants;
import org.eclipse.jetty.proxy.ConnectHandler; import org.eclipse.jetty.proxy.ConnectHandler;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Request;
/** /**
* abstract jetty http handler * abstract jetty http handler
* only request to remote hosts (proxy requests) are processed by derived classes * only request to remote hosts (proxy requests) are processed by derived classes
*/ */
abstract public class AbstractRemoteHandler extends ConnectHandler implements Handler { abstract public class AbstractRemoteHandler extends ConnectHandler {
protected Switchboard sb = null; protected Switchboard sb = null;
private final Set<String> localVirtualHostNames = new HashSet<String>(); // list for quick check for req to local peer private final Set<String> localVirtualHostNames = ConcurrentHashMap.newKeySet(); // updated by discovery thread and request threads
@Override @Override
protected void doStart() throws Exception { protected void doStart() throws Exception {
@@ -99,7 +97,7 @@ abstract public class AbstractRemoteHandler extends ConnectHandler implements Ha
}.start(); }.start();
} }
abstract public void handleRemote(String target, Request baseRequest, HttpServletRequest request, abstract public void handleRemote(String target, RequestCompletion completion, HttpServletRequest request,
HttpServletResponse response) throws IOException, ServletException; HttpServletResponse response) throws IOException, ServletException;
@Override @Override
@@ -135,7 +133,8 @@ abstract public class AbstractRemoteHandler extends ConnectHandler implements Ha
} }
final String remoteHost = request.getRemoteHost(); final String remoteHost = request.getRemoteHost();
if (!proxyippatternmatch(remoteHost)) { if (!ProxyAccessPolicy.isClientAllowed(
Switchboard.getSwitchboard().getConfig("proxyClient", "*"), remoteHost)) {
// TODO: handle proxy account // TODO: handle proxy account
response.sendError(HttpServletResponse.SC_FORBIDDEN, response.sendError(HttpServletResponse.SC_FORBIDDEN,
"proxy use not granted for IP " + remoteHost + " (see Advanced Settings -> Proxy Access Settings -> IP-Number filter)."); "proxy use not granted for IP " + remoteHost + " (see Advanced Settings -> Proxy Access Settings -> IP-Number filter).");
@@ -157,28 +156,8 @@ abstract public class AbstractRemoteHandler extends ConnectHandler implements Ha
return; return;
} }
handleRemote(target, baseRequest, request, response); handleRemote(target, () -> baseRequest.setHandled(true), request, response);
} }
/**
* helper for proxy IP config pattern check
*/
private boolean proxyippatternmatch(final String key) {
// the cfgippattern is a comma-separated list of patterns
// each pattern may contain one wildcard-character '*' which matches anything
final String cfgippattern = Switchboard.getSwitchboard().getConfig("proxyClient", "*");
if (cfgippattern.equals("*")) {
return true;
}
final StringTokenizer st = new StringTokenizer(cfgippattern, ",");
String pattern;
while (st.hasMoreTokens()) {
pattern = st.nextToken();
if (key.matches(pattern)) {
return true;
}
}
return false;
}
} }
@@ -1,3 +1,23 @@
/**
* AdminAccessPolicy
* Copyright 2026 by Michael Peter Christen
* First released 12.07.2026 at https://yacy.net
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program in the file lgpl21.txt
* If not, see <http://www.gnu.org/licenses/>.
*/
package net.yacy.http; package net.yacy.http;
import java.net.MalformedURLException; import java.net.MalformedURLException;
@@ -1,3 +1,23 @@
/**
* AdminAuthenticationContext
* Copyright 2026 by Michael Peter Christen
* First released 12.07.2026 at https://yacy.net
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program in the file lgpl21.txt
* If not, see <http://www.gnu.org/licenses/>.
*/
package net.yacy.http; package net.yacy.http;
import net.yacy.cora.protocol.Domains; import net.yacy.cora.protocol.Domains;
@@ -1,3 +1,23 @@
/**
* CrashProtectionHandler
* Copyright 2026 by Michael Peter Christen
* First released 12.07.2026 at https://yacy.net
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program in the file lgpl21.txt
* If not, see <http://www.gnu.org/licenses/>.
*/
package net.yacy.http; package net.yacy.http;
import java.io.IOException; import java.io.IOException;
@@ -8,7 +28,6 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HandlerContainer;
import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.HandlerWrapper; import org.eclipse.jetty.server.handler.HandlerWrapper;
@@ -25,7 +44,7 @@ import net.yacy.cora.util.ConcurrentLog;
* trace. Its purpose is to catch failures outside the servlet context, e.g. * trace. Its purpose is to catch failures outside the servlet context, e.g.
* in the transparent proxy handlers. * in the transparent proxy handlers.
*/ */
public class CrashProtectionHandler extends HandlerWrapper implements Handler, HandlerContainer { public class CrashProtectionHandler extends HandlerWrapper implements Handler {
public CrashProtectionHandler() { public CrashProtectionHandler() {
super(); super();
@@ -0,0 +1,103 @@
/**
* HttpServerBootstrapConfig
* Copyright 2026 by Michael Peter Christen
* First released 12.07.2026 at https://yacy.net
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program in the file lgpl21.txt
* If not, see <http://www.gnu.org/licenses/>.
*/
package net.yacy.http;
import net.yacy.search.Switchboard;
import net.yacy.search.SwitchboardConstants;
/** Immutable, servlet-container-neutral input for the embedded HTTP server. */
public final class HttpServerBootstrapConfig {
public static final int REQUEST_HEADER_SIZE = 16_384;
public static final long CONNECTOR_IDLE_TIMEOUT_MILLIS = 9_000L;
public static final int ACCEPT_QUEUE_SIZE = 128;
public static final int REQUEST_INFLATE_BUFFER_SIZE = 4_096;
public static final int MAX_FORM_CONTENT_SIZE = -1;
private final int httpPort;
private final String bindHost;
private final int acceptorCount;
private final boolean httpsEnabled;
private final int httpsPort;
private final String htrootPath;
private final String defaultsWebXml;
private final String overrideWebXml;
private final boolean gzipResponsesEnabled;
private final boolean transparentProxyEnabled;
private final String serverClientRules;
private final String adminRealm;
private HttpServerBootstrapConfig(final int httpPort, final String bindHost,
final int acceptorCount, final boolean httpsEnabled, final int httpsPort,
final String htrootPath, final String defaultsWebXml, final String overrideWebXml,
final boolean gzipResponsesEnabled, final boolean transparentProxyEnabled,
final String serverClientRules, final String adminRealm) {
this.httpPort = httpPort;
this.bindHost = bindHost;
this.acceptorCount = acceptorCount;
this.httpsEnabled = httpsEnabled;
this.httpsPort = httpsPort;
this.htrootPath = htrootPath;
this.defaultsWebXml = defaultsWebXml;
this.overrideWebXml = overrideWebXml;
this.gzipResponsesEnabled = gzipResponsesEnabled;
this.transparentProxyEnabled = transparentProxyEnabled;
this.serverClientRules = serverClientRules;
this.adminRealm = adminRealm;
}
public static HttpServerBootstrapConfig from(final Switchboard switchboard,
final int httpPort, final String bindHost) {
final int cores = Runtime.getRuntime().availableProcessors();
return new HttpServerBootstrapConfig(
httpPort,
bindHost,
acceptorCountFor(cores),
switchboard.getConfigBool("server.https", false),
switchboard.getConfigInt(SwitchboardConstants.SERVER_SSLPORT, 8443),
switchboard.appPath + "/" + switchboard.getConfig(
SwitchboardConstants.HTROOT_PATH, SwitchboardConstants.HTROOT_PATH_DEFAULT),
switchboard.appPath + "/defaults/web.xml",
switchboard.dataPath + "/DATA/SETTINGS/web.xml",
switchboard.getConfigBool(SwitchboardConstants.SERVER_RESPONSE_COMPRESS_GZIP,
SwitchboardConstants.SERVER_RESPONSE_COMPRESS_GZIP_DEFAULT),
switchboard.getConfigBool(SwitchboardConstants.PROXY_TRANSPARENT_PROXY, false),
switchboard.getConfig("serverClient", "*"),
switchboard.getConfig(SwitchboardConstants.ADMIN_REALM, "YaCy"));
}
static int acceptorCountFor(final int availableProcessors) {
return Math.max(1, Math.min(4, availableProcessors / 2));
}
public int httpPort() { return this.httpPort; }
public String bindHost() { return this.bindHost; }
public int acceptorCount() { return this.acceptorCount; }
public boolean httpsEnabled() { return this.httpsEnabled; }
public int httpsPort() { return this.httpsPort; }
public String htrootPath() { return this.htrootPath; }
public String defaultsWebXml() { return this.defaultsWebXml; }
public String overrideWebXml() { return this.overrideWebXml; }
public boolean gzipResponsesEnabled() { return this.gzipResponsesEnabled; }
public boolean transparentProxyEnabled() { return this.transparentProxyEnabled; }
public String serverClientRules() { return this.serverClientRules; }
public String adminRealm() { return this.adminRealm; }
}
@@ -1,3 +1,23 @@
/**
* InetPathAccessRule
* Copyright 2026 by Michael Peter Christen
* First released 12.07.2026 at https://yacy.net
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program in the file lgpl21.txt
* If not, see <http://www.gnu.org/licenses/>.
*/
package net.yacy.http; package net.yacy.http;
/** Container-neutral representation of a server-client address/path rule. */ /** Container-neutral representation of a server-client address/path rule. */
+20 -26
View File
@@ -52,7 +52,6 @@ import org.eclipse.jetty.server.handler.InetAccessHandler;
import org.eclipse.jetty.server.handler.gzip.GzipHandler; import org.eclipse.jetty.server.handler.gzip.GzipHandler;
import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.webapp.WebAppContext; import org.eclipse.jetty.webapp.WebAppContext;
@@ -78,6 +77,7 @@ public class Jetty9HttpServerImpl implements YaCyHttpServer {
*/ */
public Jetty9HttpServerImpl(final int port, final String host) { public Jetty9HttpServerImpl(final int port, final String host) {
final Switchboard sb = Switchboard.getSwitchboard(); final Switchboard sb = Switchboard.getSwitchboard();
final HttpServerBootstrapConfig bootstrap = HttpServerBootstrapConfig.from(sb, port, host);
this.server = new Server(); this.server = new Server();
@@ -96,32 +96,29 @@ public class Jetty9HttpServerImpl implements YaCyHttpServer {
} }
}; };
final int cores = Runtime.getRuntime().availableProcessors();
final int acceptors = Math.max(1, Math.min(4, cores/2)); // original: Math.max(1, Math.min(4,cores/8));
final HttpConfiguration httpConfig = new HttpConfiguration(); final HttpConfiguration httpConfig = new HttpConfiguration();
httpConfig.setRequestHeaderSize(16384); httpConfig.setRequestHeaderSize(HttpServerBootstrapConfig.REQUEST_HEADER_SIZE);
final HttpConnectionFactory hcf = new HttpConnectionFactory(httpConfig); final HttpConnectionFactory hcf = new HttpConnectionFactory(httpConfig);
final ServerConnector connector = new ServerConnector(this.server, null, null, null, acceptors, -1, hcf); final ServerConnector connector = new ServerConnector(this.server, null, null, null, bootstrap.acceptorCount(), -1, hcf);
connector.setPort(port); connector.setPort(bootstrap.httpPort());
connector.setHost(host); connector.setHost(bootstrap.bindHost());
connector.setName("httpd-" + host + ":" + Integer.toString(port)); connector.setName("httpd-" + bootstrap.bindHost() + ":" + Integer.toString(bootstrap.httpPort()));
connector.setIdleTimeout(9000); // timout in ms when no bytes send / received connector.setIdleTimeout(HttpServerBootstrapConfig.CONNECTOR_IDLE_TIMEOUT_MILLIS);
connector.setAcceptQueueSize(128); connector.setAcceptQueueSize(HttpServerBootstrapConfig.ACCEPT_QUEUE_SIZE);
connector.addBean(connectionCloseMonitor); connector.addBean(connectionCloseMonitor);
this.server.addConnector(connector); this.server.addConnector(connector);
// add ssl/https connector // add ssl/https connector
final boolean useSSL = sb.getConfigBool("server.https", false); final boolean useSSL = bootstrap.httpsEnabled();
if (useSSL) { if (useSSL) {
final SslContextFactory sslContextFactory = new SslContextFactory.Server(); final SslContextFactory sslContextFactory = new SslContextFactory.Server();
final SSLContext sslContext = this.initSslContext(sb); final SSLContext sslContext = this.initSslContext(sb);
if (sslContext != null) { if (sslContext != null) {
final int sslport = sb.getConfigInt(SwitchboardConstants.SERVER_SSLPORT, 8443); final int sslport = bootstrap.httpsPort();
sslContextFactory.setSslContext(sslContext); sslContextFactory.setSslContext(sslContext);
// SSL HTTP Configuration // SSL HTTP Configuration
@@ -134,7 +131,7 @@ public class Jetty9HttpServerImpl implements YaCyHttpServer {
new HttpConnectionFactory(https_config)); new HttpConnectionFactory(https_config));
sslConnector.setPort(sslport); sslConnector.setPort(sslport);
sslConnector.setName("ssld:" + Integer.toString(sslport)); // name must start with ssl (for withSSL() to work correctly) sslConnector.setName("ssld:" + Integer.toString(sslport)); // name must start with ssl (for withSSL() to work correctly)
sslConnector.setIdleTimeout(9000); // timout in ms when no bytes send / received sslConnector.setIdleTimeout(HttpServerBootstrapConfig.CONNECTOR_IDLE_TIMEOUT_MILLIS);
sslConnector.addBean(connectionCloseMonitor); sslConnector.addBean(connectionCloseMonitor);
this.server.addConnector(sslConnector); this.server.addConnector(sslConnector);
@@ -148,7 +145,7 @@ public class Jetty9HttpServerImpl implements YaCyHttpServer {
// configure root context // configure root context
final WebAppContext htrootContext = new WebAppContext(); final WebAppContext htrootContext = new WebAppContext();
htrootContext.setContextPath("/"); htrootContext.setContextPath("/");
final String htrootpath = sb.appPath + "/" + sb.getConfig(SwitchboardConstants.HTROOT_PATH, SwitchboardConstants.HTROOT_PATH_DEFAULT); final String htrootpath = bootstrap.htrootPath();
ConcurrentLog.info("Jetty9HttpServerImpl", "htrootpath = " + htrootpath); ConcurrentLog.info("Jetty9HttpServerImpl", "htrootpath = " + htrootpath);
htrootContext.setErrorHandler(new YaCyErrorHandler()); // handler for custom error page htrootContext.setErrorHandler(new YaCyErrorHandler()); // handler for custom error page
try { try {
@@ -158,8 +155,8 @@ public class Jetty9HttpServerImpl implements YaCyHttpServer {
// make use of Jetty feature to define web.xml other as default WEB-INF/web.xml // make use of Jetty feature to define web.xml other as default WEB-INF/web.xml
// and to use a DefaultsDescriptor merged with a individual web.xml // and to use a DefaultsDescriptor merged with a individual web.xml
// use defaults/web.xml as default and look in DATA/SETTINGS for local addition/changes // use defaults/web.xml as default and look in DATA/SETTINGS for local addition/changes
htrootContext.setDefaultsDescriptor(sb.appPath + "/defaults/web.xml"); htrootContext.setDefaultsDescriptor(bootstrap.defaultsWebXml());
final Resource webxml = Resource.newResource(sb.dataPath + "/DATA/SETTINGS/web.xml"); final Resource webxml = Resource.newResource(bootstrap.overrideWebXml());
if (webxml.exists()) { if (webxml.exists()) {
htrootContext.setDescriptor(webxml.getName()); htrootContext.setDescriptor(webxml.getName());
} }
@@ -191,10 +188,9 @@ public class Jetty9HttpServerImpl implements YaCyHttpServer {
* APIs /yacy/transferRWI.html and /yacy/transferURL.html This was previously * APIs /yacy/transferRWI.html and /yacy/transferURL.html This was previously
* handled by a GZIPRequestWrapper in the YaCyDefaultServlet. * handled by a GZIPRequestWrapper in the YaCyDefaultServlet.
*/ */
gzipHandler.setInflateBufferSize(4096); gzipHandler.setInflateBufferSize(HttpServerBootstrapConfig.REQUEST_INFLATE_BUFFER_SIZE);
if (!sb.getConfigBool(SwitchboardConstants.SERVER_RESPONSE_COMPRESS_GZIP, if (!bootstrap.gzipResponsesEnabled()) {
SwitchboardConstants.SERVER_RESPONSE_COMPRESS_GZIP_DEFAULT)) {
/* Gzip compression of responses can be disabled by user configuration */ /* Gzip compression of responses can be disabled by user configuration */
gzipHandler.setExcludedMethods(HttpMethod.GET.asString(), HttpMethod.POST.asString()); gzipHandler.setExcludedMethods(HttpMethod.GET.asString(), HttpMethod.POST.asString());
} }
@@ -224,7 +220,7 @@ public class Jetty9HttpServerImpl implements YaCyHttpServer {
// define list of YaCy specific general handlers // define list of YaCy specific general handlers
final HandlerList handlers = new HandlerList(); final HandlerList handlers = new HandlerList();
if (sb.getConfigBool(SwitchboardConstants.PROXY_TRANSPARENT_PROXY, false)) { if (bootstrap.transparentProxyEnabled()) {
// Proxyhandlers are only needed if feature activated (save resources if not used) // Proxyhandlers are only needed if feature activated (save resources if not used)
ConcurrentLog.info("SERVER", "load Jetty handler for transparent proxy"); ConcurrentLog.info("SERVER", "load Jetty handler for transparent proxy");
handlers.setHandlers(new Handler[]{domainHandler, new ProxyCacheHandler(), new ProxyHandler()}); handlers.setHandlers(new Handler[]{domainHandler, new ProxyCacheHandler(), new ProxyHandler()});
@@ -236,9 +232,7 @@ public class Jetty9HttpServerImpl implements YaCyHttpServer {
context.setServer(this.server); context.setServer(this.server);
context.setContextPath("/"); context.setContextPath("/");
context.setHandler(handlers); context.setHandler(handlers);
context.setMaxFormContentSize(-1); context.setMaxFormContentSize(HttpServerBootstrapConfig.MAX_FORM_CONTENT_SIZE);
final org.eclipse.jetty.util.log.Logger log = Log.getRootLogger();
context.setLogger(log);
// make YaCy handlers (in context) and servlet context handlers available (both contain root context "/") // make YaCy handlers (in context) and servlet context handlers available (both contain root context "/")
// logic: 1. YaCy handlers are called if request not handled (e.g. proxy) then servlets handle it // logic: 1. YaCy handlers are called if request not handled (e.g. proxy) then servlets handle it
final ContextHandlerCollection allrequesthandlers = new ContextHandlerCollection(); final ContextHandlerCollection allrequesthandlers = new ContextHandlerCollection();
@@ -250,7 +244,7 @@ public class Jetty9HttpServerImpl implements YaCyHttpServer {
final YaCyLoginService loginService = new YaCyLoginService(); final YaCyLoginService loginService = new YaCyLoginService();
// This is part of the built-in administrator's DIGEST password hash. // This is part of the built-in administrator's DIGEST password hash.
// Changing it invalidates the configured administrator password hash. // Changing it invalidates the configured administrator password hash.
loginService.setName(sb.getConfig(SwitchboardConstants.ADMIN_REALM,"YaCy")); loginService.setName(bootstrap.adminRealm());
final YaCySecurityHandler securityHandler = new YaCySecurityHandler(); final YaCySecurityHandler securityHandler = new YaCySecurityHandler();
securityHandler.setLoginService(loginService); securityHandler.setLoginService(loginService);
@@ -261,7 +255,7 @@ public class Jetty9HttpServerImpl implements YaCyHttpServer {
final Handler crashHandler = new CrashProtectionHandler(this.server, allrequesthandlers); final Handler crashHandler = new CrashProtectionHandler(this.server, allrequesthandlers);
// check server access restriction and add InetAccessHandler if restrictions are needed // check server access restriction and add InetAccessHandler if restrictions are needed
// otherwise don't (to save performance) // otherwise don't (to save performance)
final String white = sb.getConfig("serverClient", "*"); final String white = bootstrap.serverClientRules();
if (!white.equals("*")) { // full ip (allowed ranges 0-255 or prefix 10.0-255,0,0-100 or CIDR notation 192.168.1.0/24) if (!white.equals("*")) { // full ip (allowed ranges 0-255 or prefix 10.0-255,0,0-100 or CIDR notation 192.168.1.0/24)
final StringTokenizer st = new StringTokenizer(white, ","); final StringTokenizer st = new StringTokenizer(white, ",");
final InetAccessHandler whiteListHandler; final InetAccessHandler whiteListHandler;
@@ -0,0 +1,43 @@
/**
* ProxyAccessPolicy
* Copyright 2026 by Michael Peter Christen
* First released 12.07.2026 at https://yacy.net
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program in the file lgpl21.txt
* If not, see <http://www.gnu.org/licenses/>.
*/
package net.yacy.http;
/** Container-neutral checks for the configured transparent-proxy client list. */
public final class ProxyAccessPolicy {
private ProxyAccessPolicy() {
}
public static boolean isClientAllowed(final String configuredPatterns, final String clientHost) {
if ("*".equals(configuredPatterns)) {
return true;
}
if (configuredPatterns == null || configuredPatterns.isEmpty() || clientHost == null) {
return false;
}
for (final String pattern : configuredPatterns.split(",")) {
if (!pattern.isEmpty() && clientHost.matches(pattern)) {
return true;
}
}
return false;
}
}
+3 -5
View File
@@ -31,8 +31,6 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import net.yacy.cora.document.id.DigestURL; import net.yacy.cora.document.id.DigestURL;
import net.yacy.cora.protocol.RequestHeader; import net.yacy.cora.protocol.RequestHeader;
@@ -43,7 +41,7 @@ import net.yacy.crawler.retrieval.Response;
/** /**
* jetty http handler serves pages from cache if available and valid * jetty http handler serves pages from cache if available and valid
*/ */
public class ProxyCacheHandler extends AbstractRemoteHandler implements Handler { public class ProxyCacheHandler extends AbstractRemoteHandler {
private void handleRequestFromCache(@SuppressWarnings("unused") HttpServletRequest request, HttpServletResponse response, ResponseHeader cachedResponseHeader, byte[] content) throws IOException { private void handleRequestFromCache(@SuppressWarnings("unused") HttpServletRequest request, HttpServletResponse response, ResponseHeader cachedResponseHeader, byte[] content) throws IOException {
@@ -57,7 +55,7 @@ public class ProxyCacheHandler extends AbstractRemoteHandler implements Handler
} }
@Override @Override
public void handleRemote(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { public void handleRemote(String target, RequestCompletion completion, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
if (request.getMethod().equals("GET")) { if (request.getMethod().equals("GET")) {
String queryString = request.getQueryString() != null ? "?" + request.getQueryString() : ""; String queryString = request.getQueryString() != null ? "?" + request.getQueryString() : "";
DigestURL url = new DigestURL(request.getRequestURL().toString() + queryString); DigestURL url = new DigestURL(request.getRequestURL().toString() + queryString);
@@ -86,7 +84,7 @@ public class ProxyCacheHandler extends AbstractRemoteHandler implements Handler
byte[] cacheContent = Cache.getContent(url.hash()); byte[] cacheContent = Cache.getContent(url.hash());
if (cacheContent != null && cachedResponse.isFreshForProxy()) { if (cacheContent != null && cachedResponse.isFreshForProxy()) {
handleRequestFromCache(request, response, cachedResponseHeader, cacheContent); handleRequestFromCache(request, response, cachedResponseHeader, cacheContent);
baseRequest.setHandled(true); completion.complete();
} }
} }
+3 -5
View File
@@ -52,14 +52,12 @@ import net.yacy.server.http.MultiOutputStream;
import org.apache.http.Header; import org.apache.http.Header;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
/** /**
* jetty http handler * jetty http handler
* proxies request, caches responses and adds urls to crawler * proxies request, caches responses and adds urls to crawler
*/ */
public class ProxyHandler extends AbstractRemoteHandler implements Handler { public class ProxyHandler extends AbstractRemoteHandler {
protected int timeout = 10000; protected int timeout = 10000;
@@ -124,7 +122,7 @@ public class ProxyHandler extends AbstractRemoteHandler implements Handler {
} }
@Override @Override
public void handleRemote(String target, Request baseRequest, HttpServletRequest request, public void handleRemote(String target, RequestCompletion completion, HttpServletRequest request,
HttpServletResponse response) throws IOException, ServletException { HttpServletResponse response) throws IOException, ServletException {
sb.proxyLastAccess = System.currentTimeMillis(); sb.proxyLastAccess = System.currentTimeMillis();
@@ -222,7 +220,7 @@ public class ProxyHandler extends AbstractRemoteHandler implements Handler {
// we handled this request, break out of handler chain // we handled this request, break out of handler chain
logProxyAccess(request); logProxyAccess(request);
baseRequest.setHandled(true); completion.complete();
} }
/** /**
@@ -0,0 +1,28 @@
/**
* RequestCompletion
* Copyright 2026 by Michael Peter Christen
* First released 12.07.2026 at https://yacy.net
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program in the file lgpl21.txt
* If not, see <http://www.gnu.org/licenses/>.
*/
package net.yacy.http;
/** Container-neutral callback used when a handler has completed a request. */
@FunctionalInterface
public interface RequestCompletion {
void complete();
}
+11 -14
View File
@@ -28,51 +28,48 @@ package net.yacy.http;
*/ */
public interface YaCyHttpServer { public interface YaCyHttpServer {
/** /** Start all configured connectors and handlers before returning. */
* start the http server
*/
void startupServer() throws Exception; void startupServer() throws Exception;
/** /** Stop all connectors and handlers and wait for complete termination. */
* stop the http server
*/
void stop() throws Exception; void stop() throws Exception;
/** /**
* reconnect with new port settings (after waiting milsec) - routine returns immediately * Apply current HTTP and HTTPS port settings asynchronously after a delay.
* @param milsec wait time * Existing connectors are reused; implementations must not rebuild the handler graph.
* @param milsec non-negative delay before applying current configuration
*/ */
void reconnect(int milsec); void reconnect(int milsec);
/** /**
* @return true if the server runs a ssl/https connector * @return true when a usable HTTPS connector was configured
*/ */
boolean withSSL(); boolean withSSL();
/** /**
* @return the ssl/https port or -1 if not active * @return the bound HTTPS port, or -1 when HTTPS is not active
*/ */
int getSslPort(); int getSslPort();
/** /**
* forces loginservice to reload user credentials * Evict and immediately reload the named administrator identity from configuration.
* @param username * @param username
*/ */
void resetUser(String username); void resetUser(String username);
/** /**
* removes user from the loginservice * Evict the named administrator identity from the container login cache.
* @param username * @param username
*/ */
void removeUser(String username); void removeUser(String username);
/** /**
* @return version string of the servlet container * @return human-readable name and version of the servlet container
*/ */
String getVersion(); String getVersion();
/** /**
* @return the number of currently active (busy) server threads * @return current number of non-idle container worker threads
*/ */
int getServerThreads(); int getServerThreads();
} }
+1 -2
View File
@@ -38,7 +38,6 @@ import javax.servlet.http.HttpServletResponse;
import net.yacy.cora.protocol.Domains; import net.yacy.cora.protocol.Domains;
import net.yacy.server.http.AlternativeDomainNames; import net.yacy.server.http.AlternativeDomainNames;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.handler.AbstractHandler;
@@ -53,7 +52,7 @@ import org.eclipse.jetty.server.handler.AbstractHandler;
* host is picked up and forwarded to the peer by the transparent proxy * host is picked up and forwarded to the peer by the transparent proxy
* handlers, before the local servlet context would handle it. * handlers, before the local servlet context would handle it.
*/ */
public class YacyDomainHandler extends AbstractHandler implements Handler { public class YacyDomainHandler extends AbstractHandler {
private AlternativeDomainNames alternativeResolvers; private AlternativeDomainNames alternativeResolvers;
@@ -1,3 +1,23 @@
/**
* Jetty9ServletResource
* Copyright 2026 by Michael Peter Christen
* First released 12.07.2026 at https://yacy.net
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program in the file lgpl21.txt
* If not, see <http://www.gnu.org/licenses/>.
*/
package net.yacy.http.servlets; package net.yacy.http.servlets;
import java.io.File; import java.io.File;
@@ -1,3 +1,23 @@
/**
* ServletResource
* Copyright 2026 by Michael Peter Christen
* First released 12.07.2026 at https://yacy.net
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program in the file lgpl21.txt
* If not, see <http://www.gnu.org/licenses/>.
*/
package net.yacy.http.servlets; package net.yacy.http.servlets;
import java.io.IOException; import java.io.IOException;
@@ -1,3 +1,23 @@
/**
* UrlProxyServlet
* Copyright 2026 by Michael Peter Christen
* First released 12.07.2026 at https://yacy.net
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program in the file lgpl21.txt
* If not, see <http://www.gnu.org/licenses/>.
*/
package net.yacy.http.servlets; package net.yacy.http.servlets;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
@@ -0,0 +1,32 @@
package net.yacy.cora.federate.solr.connector;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.params.ModifiableSolrParams;
import net.yacy.cora.federate.solr.instance.RemoteInstance;
/** Command-line integration probe for YaCY's Apache-based remote Solr path. */
public final class RemoteSolrSmoke {
private RemoteSolrSmoke() {
}
public static void main(final String[] args) throws Exception {
if (args.length != 1) {
throw new IllegalArgumentException("usage: RemoteSolrSmoke SOLR_BASE_URL");
}
final RemoteInstance instance = new RemoteInstance(
args[0], null, "collection1", 10_000, false, Long.MAX_VALUE, false);
try {
final RemoteSolrConnector connector = new RemoteSolrConnector(instance, false);
final QueryResponse response = connector.getResponseByParams(
new ModifiableSolrParams().set("q", "*:*").set("rows", 0));
if (response.getResults() == null) {
throw new IllegalStateException("remote Solr response has no result list");
}
System.out.println("Remote Solr numFound=" + response.getResults().getNumFound());
} finally {
instance.close();
}
}
}
@@ -0,0 +1,25 @@
package net.yacy.http;
import org.junit.Assert;
import org.junit.Test;
public class HttpServerBootstrapConfigTest {
@Test
public void testAcceptorCountIsClamped() {
Assert.assertEquals(1, HttpServerBootstrapConfig.acceptorCountFor(1));
Assert.assertEquals(1, HttpServerBootstrapConfig.acceptorCountFor(2));
Assert.assertEquals(2, HttpServerBootstrapConfig.acceptorCountFor(4));
Assert.assertEquals(4, HttpServerBootstrapConfig.acceptorCountFor(8));
Assert.assertEquals(4, HttpServerBootstrapConfig.acceptorCountFor(64));
}
@Test
public void testFixedConnectorLimits() {
Assert.assertEquals(16_384, HttpServerBootstrapConfig.REQUEST_HEADER_SIZE);
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);
}
}
@@ -0,0 +1,16 @@
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());
}
}
@@ -0,0 +1,20 @@
package net.yacy.http;
import org.junit.Assert;
import org.junit.Test;
public class ProxyAccessPolicyTest {
@Test
public void testConfiguredClientPatterns() {
Assert.assertTrue(ProxyAccessPolicy.isClientAllowed("*", "198.51.100.7"));
Assert.assertTrue(ProxyAccessPolicy.isClientAllowed(
"localhost,127\\.0\\.0\\.1,192\\.168\\..*", "127.0.0.1"));
Assert.assertTrue(ProxyAccessPolicy.isClientAllowed(
"localhost,127\\.0\\.0\\.1,192\\.168\\..*", "192.168.2.15"));
Assert.assertFalse(ProxyAccessPolicy.isClientAllowed(
"localhost,127\\.0\\.0\\.1,192\\.168\\..*", "198.51.100.7"));
Assert.assertFalse(ProxyAccessPolicy.isClientAllowed(null, "127.0.0.1"));
Assert.assertFalse(ProxyAccessPolicy.isClientAllowed("127\\.0\\.0\\.1", null));
}
}
@@ -0,0 +1,59 @@
package net.yacy.http;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import org.junit.Test;
import org.slf4j.LoggerFactory;
/** Version-neutral contract for the public SLF4J 2 to JUL logging path. */
public class Slf4jJulBridgeTest {
@Test
public void publicSlf4jProviderRoutesJettyNamespaceToJul() {
assertEquals("org.slf4j.jul.JDK14LoggerFactory",
LoggerFactory.getILoggerFactory().getClass().getName());
final String loggerName = "org.eclipse.jetty.yacy.logging.test";
final Logger julLogger = Logger.getLogger(loggerName);
final Level previousLevel = julLogger.getLevel();
final boolean previousUseParentHandlers = julLogger.getUseParentHandlers();
final AtomicReference<LogRecord> received = new AtomicReference<>();
final Handler capture = new Handler() {
@Override
public void publish(final LogRecord record) {
received.set(record);
}
@Override
public void flush() {
}
@Override
public void close() {
}
};
try {
julLogger.setUseParentHandlers(false);
julLogger.setLevel(Level.INFO);
capture.setLevel(Level.ALL);
julLogger.addHandler(capture);
LoggerFactory.getLogger(loggerName).info("Jetty SLF4J to JUL bridge test");
assertTrue("SLF4J log record did not reach java.util.logging", received.get() != null);
assertEquals("Jetty SLF4J to JUL bridge test", received.get().getMessage());
} finally {
julLogger.removeHandler(capture);
julLogger.setLevel(previousLevel);
julLogger.setUseParentHandlers(previousUseParentHandlers);
}
}
}
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env sh
# Authentication contract for the embedded server. A remote URL must really
# reach the peer through a non-loopback socket; forwarded headers are not used.
set -eu
base_url=${YACY_SMOKE_BASE_URL:-http://127.0.0.1:8090}
remote_base_url=${YACY_SMOKE_REMOTE_BASE_URL:-}
protected_path=${YACY_SMOKE_PROTECTED_PATH:-/ConfigAccounts_p.html}
conf=${YACY_SMOKE_CONF:-DATA/SETTINGS/yacy.conf}
timeout=${YACY_SMOKE_TIMEOUT:-30}
require_complete=${YACY_SMOKE_REQUIRE_COMPLETE:-false}
case "$require_complete" in
true|false) ;;
*) echo "FAIL: YACY_SMOKE_REQUIRE_COMPLETE must be true or false" >&2; exit 2 ;;
esac
[ -f "$conf" ] || { echo "FAIL: missing peer configuration: $conf" >&2; exit 2; }
status() {
curl --silent --show-error --max-time "$timeout" --output /dev/null \
--write-out '%{http_code}' "$@"
}
localhost_access=$(sed -n 's/^adminAccountForLocalhost=//p' "$conf" | head -n 1)
local_status=$(status "$base_url$protected_path")
if [ "$localhost_access" = "true" ]; then
[ "$local_status" = 200 ] || { echo "FAIL: localhost bypass returned $local_status" >&2; exit 1; }
else
[ "$local_status" = 401 ] || { echo "FAIL: protected localhost request returned $local_status" >&2; exit 1; }
fi
echo "ok 1 - configured localhost access rule"
YACY_DATA_PATH=$(CDPATH= cd -- "$(dirname "$conf")/.." && pwd)
export YACY_DATA_PATH
bin/apicall.sh 'ConfigAccounts_p.html' >/dev/null
echo "ok 2 - bin/apicall.sh localhost authentication"
if [ -n "${YACY_SMOKE_ADMIN_USER:-}" ] && [ -n "${YACY_SMOKE_ADMIN_PASSWORD:-}" ]; then
authenticated_status=$(status --user "$YACY_SMOKE_ADMIN_USER:$YACY_SMOKE_ADMIN_PASSWORD" \
"$base_url$protected_path")
[ "$authenticated_status" = 200 ] || { echo "FAIL: administrator login returned $authenticated_status" >&2; exit 1; }
echo "ok 3 - administrator credentials"
else
if [ "$require_complete" = true ]; then
echo "FAIL: complete acceptance requires YACY_SMOKE_ADMIN_USER and YACY_SMOKE_ADMIN_PASSWORD" >&2
exit 1
fi
echo "ok 3 # SKIP - set YACY_SMOKE_ADMIN_USER and YACY_SMOKE_ADMIN_PASSWORD"
fi
if [ -n "$remote_base_url" ]; then
remote_status=$(status "$remote_base_url$protected_path")
[ "$remote_status" = 401 ] || { echo "FAIL: remote protected request returned $remote_status" >&2; exit 1; }
echo "ok 4 - remote request does not receive localhost bypass"
else
if [ "$require_complete" = true ]; then
echo "FAIL: complete acceptance requires YACY_SMOKE_REMOTE_BASE_URL over a real non-loopback path" >&2
exit 1
fi
echo "ok 4 # SKIP - set YACY_SMOKE_REMOTE_BASE_URL to a real non-loopback path"
fi
echo "PASS: embedded-server authentication contract"
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env sh
# Lifecycle gate owned by an external isolated-peer harness. Commands are
# explicit because this script must never start or stop the developer's normal
# DATA directory by guessing.
set -eu
[ -n "${YACY_SMOKE_START_COMMAND:-}" ] && [ -n "${YACY_SMOKE_STOP_COMMAND:-}" ] || {
echo "SKIP: set YACY_SMOKE_START_COMMAND and YACY_SMOKE_STOP_COMMAND for an isolated DATA directory" >&2
exit 2
}
base_url=${YACY_SMOKE_BASE_URL:-http://127.0.0.1:8090}
timeout=${YACY_SMOKE_TIMEOUT:-60}
started=false
cleanup() {
if [ "$started" = true ]; then
sh -c "$YACY_SMOKE_STOP_COMMAND"
fi
}
trap cleanup EXIT HUP INT TERM
sh -c "$YACY_SMOKE_START_COMMAND"
started=true
attempt=0
while [ "$attempt" -lt "$timeout" ]; do
if curl --silent --fail --max-time 2 "$base_url/api/version.xml" >/dev/null 2>&1; then
break
fi
attempt=$((attempt + 1))
sleep 1
done
[ "$attempt" -lt "$timeout" ] || { echo "FAIL: isolated peer did not start" >&2; exit 1; }
test/jetty-smoke-test.sh "$base_url"
curl --silent --show-error --fail --max-time 30 \
"$base_url/solr/collection1/select?q=*:*&rows=0&wt=json" \
| grep -F 'numFound' >/dev/null || {
echo "FAIL: embedded Solr query failed" >&2
exit 1
}
echo "ok - embedded Solr query"
cleanup
started=false
trap - EXIT HUP INT TERM
echo "PASS: isolated peer start, embedded Solr and clean stop"
+93 -16
View File
@@ -53,21 +53,35 @@ fail() {
request() { request() {
method=$1 method=$1
path=$2 path=$2
shift 2 expected_status=$3
shift 3
: > "$response_headers" : > "$response_headers"
: > "$response_body" : > "$response_body"
status=$(curl --silent --show-error \ if [ "$method" = HEAD ]; then
--max-time "$curl_timeout" \ status=$(curl --silent --show-error \
--request "$method" \ --max-time "$curl_timeout" \
--dump-header "$response_headers" \ --head \
--output "$response_body" \ --dump-header "$response_headers" \
--write-out '%{http_code}' \ --output /dev/null \
"$@" \ --write-out '%{http_code}' \
"$base_url$path") || fail "$method $path could not be requested" "$@" \
"$base_url$path") || fail "$method $path could not be requested"
: > "$response_body"
else
status=$(curl --silent --show-error \
--max-time "$curl_timeout" \
--request "$method" \
--dump-header "$response_headers" \
--output "$response_body" \
--write-out '%{http_code}' \
"$@" \
"$base_url$path") || fail "$method $path could not be requested"
fi
[ "$status" = "200" ] || fail "$method $path returned HTTP $status" [ "$status" = "$expected_status" ] || \
fail "$method $path returned HTTP $status (expected $expected_status)"
} }
assert_header_contains() { assert_header_contains() {
@@ -109,38 +123,101 @@ while [ "$attempt" -lt 30 ]; do
done done
[ "$attempt" -lt 30 ] || fail "YaCy did not become ready within 30 seconds" [ "$attempt" -lt 30 ] || fail "YaCy did not become ready within 30 seconds"
request GET /api/version.xml request GET /api/version.xml 200
assert_header_contains Content-Type text/xml assert_header_contains Content-Type text/xml
assert_body_contains '<version>' assert_body_contains '<version>'
assert_body_contains '<buildVersion>' assert_body_contains '<buildVersion>'
pass "reflection-backed XML endpoint" pass "reflection-backed XML endpoint"
request GET /env/grafics/YaCyLogo2012.svg request GET /env/grafics/YaCyLogo2012.svg 200
assert_header_contains Content-Type image/svg+xml assert_header_contains Content-Type image/svg+xml
assert_body_contains '<svg' assert_body_contains '<svg'
pass "static resource" pass "static resource"
request GET /index.html cp "$response_body" "$work_dir/static-full"
static_length=$(wc -c < "$work_dir/static-full" | tr -d ' ')
request HEAD /env/grafics/YaCyLogo2012.svg 200
assert_header_contains Content-Type image/svg+xml
assert_header_contains Content-Length "$static_length"
[ ! -s "$response_body" ] || fail "HEAD response contains a body"
pass "HEAD without response body"
request GET /env/grafics/YaCyLogo2012.svg 206 --header 'Range: bytes=0-9'
assert_header_contains Content-Range "bytes 0-9/$static_length"
[ "$(wc -c < "$response_body" | tr -d ' ')" -eq 10 ] || fail "range body is not 10 bytes"
head -c 10 "$work_dir/static-full" > "$work_dir/static-prefix"
cmp "$work_dir/static-prefix" "$response_body" >/dev/null 2>&1 || fail "range body has unexpected bytes"
pass "single byte range"
request GET /env/grafics/YaCyLogo2012.svg 206 --header 'Range: bytes=0-4,48-51'
assert_header_contains Content-Type 'multipart/byteranges; boundary='
assert_body_contains "Content-Range: bytes 0-4/$static_length"
assert_body_contains "Content-Range: bytes 48-51/$static_length"
assert_body_contains '<?xml'
assert_body_contains 'YaCy'
multipart_boundary=$(tr -d '\r' < "$response_headers" | awk -F 'boundary=' '
tolower($0) ~ /^content-type: multipart\/byteranges/ { print $2 }
' | tail -n 1)
[ -n "$multipart_boundary" ] || fail "multipart range response has no boundary"
tail -c 80 "$response_body" | grep -F -- "--$multipart_boundary--" >/dev/null 2>&1 || \
fail "multipart range response has no closing boundary"
pass "multipart byte ranges"
request GET /env/grafics/YaCyLogo2012.svg 416 --header "Range: bytes=$static_length-"
assert_header_contains Content-Range "bytes */$static_length"
pass "unsatisfiable byte range"
# YaCy deliberately suppresses Last-Modified on static responses to control its
# cache policy, but still implements If-Modified-Since against the resource.
request GET /env/grafics/YaCyLogo2012.svg 304 \
--header 'If-Modified-Since: Thu, 31 Dec 2099 23:59:59 GMT'
[ ! -s "$response_body" ] || fail "304 response contains a body"
pass "If-Modified-Since"
request GET /index.html 200
assert_header_contains Content-Type text/html assert_header_contains Content-Type text/html
assert_body_contains '<!DOCTYPE html>' assert_body_contains '<!DOCTYPE html>'
assert_body_contains '<html' assert_body_contains '<html'
pass "rendered HTML template" pass "rendered HTML template"
request GET '/suggest.json?q=jetty-smoke' request GET '/suggest.json?q=jetty-smoke' 200
assert_header_contains Content-Type application/json assert_header_contains Content-Type application/json
assert_header_contains Access-Control-Allow-Origin '*' assert_header_contains Access-Control-Allow-Origin '*'
assert_body_contains '["jetty-smoke",[' assert_body_contains '["jetty-smoke",['
pass "reflection-backed JSON endpoint and CORS header" pass "reflection-backed JSON endpoint and CORS header"
request POST /api/version.xml \ request POST /api/version.xml 200 \
--header 'Content-Type: application/x-www-form-urlencoded' \ --header 'Content-Type: application/x-www-form-urlencoded' \
--data 'smoke=post' --data 'smoke=post'
assert_header_contains Content-Type text/xml assert_header_contains Content-Type text/xml
assert_body_contains '<version>' assert_body_contains '<version>'
pass "URL-encoded POST dispatch" pass "URL-encoded POST dispatch"
request OPTIONS /api/version.xml request OPTIONS /api/version.xml 200
assert_header_contains Allow 'GET,HEAD,POST,OPTIONS' assert_header_contains Allow 'GET,HEAD,POST,OPTIONS'
pass "OPTIONS method contract" pass "OPTIONS method contract"
request GET /this-resource-must-not-exist-yacy-jetty-smoke 404
assert_header_contains Content-Type text/html
assert_body_contains 'YaCy '
pass "YaCy 404 error page"
request GET /env/grafics/YaCyLogo2012.svg 200 \
--header 'Accept-Encoding: gzip' \
--raw
assert_header_contains Content-Encoding gzip
gzip -dc "$response_body" > "$work_dir/gzip-decoded" || fail "gzip response cannot be decompressed"
cmp "$work_dir/static-full" "$work_dir/gzip-decoded" >/dev/null 2>&1 || \
fail "decompressed response differs from the uncompressed resource"
pass "gzip response compression"
printf 'q=jetty-gzip-smoke' > "$work_dir/gzip-request-form"
gzip -c "$work_dir/gzip-request-form" > "$work_dir/gzip-request-body"
request POST /suggest.json 200 \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Content-Encoding: gzip' \
--data-binary "@$work_dir/gzip-request-body"
assert_body_contains '["jetty-gzip-smoke",['
pass "gzip request decompression"
echo "PASS: $checks embedded-server checks succeeded." echo "PASS: $checks embedded-server checks succeeded."
+69 -3
View File
@@ -42,14 +42,72 @@ for artifact in http2-client http2-common http2-http-client-transport; do
fail "$artifact must only be an input of the Solr 9 bridge" fail "$artifact must only be an input of the Solr 9 bridge"
done done
for artifact in slf4j-api slf4j-jdk14; do
grep -E "name=\"$artifact\" rev=\"1.7.36\" conf=\"solr9-bridge->master\"" ivy.xml >/dev/null 2>&1 || \
fail "$artifact 1.7.36 must only be an input of the Solr 9 bridge"
done
grep -E 'org="org.eclipse.jetty.toolchain" name="jetty-servlet-api" rev="4.0.9"' ivy.xml >/dev/null 2>&1 || \
fail "Jetty's EE8 Servlet 4 API must be an explicit dependency"
grep -E 'exclude org="javax.servlet" module="javax.servlet-api"' ivy.xml >/dev/null 2>&1 || \
fail "transitive javax.servlet-api artifacts must be excluded"
expected_jetty_version=$(sed -n \
's/.*org="org.eclipse.jetty" name="jetty-server" rev="\([^"]*\)".*/\1/p' \
ivy.xml)
[ -n "$expected_jetty_version" ] || \
fail "could not determine the public Jetty version from jetty-server in ivy.xml"
[ "$(printf '%s\n' "$expected_jetty_version" | wc -l | tr -d ' ')" -eq 1 ] || \
fail "jetty-server must declare exactly one public Jetty version"
if [ -d lib ]; then if [ -d lib ]; then
public_jetty_count=0
for artifact in lib/jetty-*.jar; do
[ -e "$artifact" ] || continue
case $(basename "$artifact") in
jetty-servlet-api-*.jar)
# Jetty's Servlet 4 toolchain has its own version line.
continue
;;
esac
public_jetty_count=$((public_jetty_count + 1))
case $(basename "$artifact") in
*-"$expected_jetty_version".jar) ;;
*) fail "public Jetty artifact is not on version $expected_jetty_version: $artifact" ;;
esac
done
[ "$public_jetty_count" -gt 0 ] || \
fail "no public Jetty $expected_jetty_version artifacts found"
case "$expected_jetty_version" in
12.*)
for artifact in lib/jetty-continuation-*.jar; do
[ -e "$artifact" ] || continue
fail "Jetty 9-only artifact remains on the Jetty 12 classpath: $artifact"
done
;;
esac
servlet_api_count=0
for artifact in lib/*servlet-api-*.jar; do
[ -e "$artifact" ] || continue
servlet_api_count=$((servlet_api_count + 1))
[ "$(basename "$artifact")" = "jetty-servlet-api-4.0.9.jar" ] || \
fail "unexpected Servlet API artifact: $artifact"
done
[ "$servlet_api_count" -eq 1 ] || \
fail "expected exactly one public Servlet API artifact, found $servlet_api_count"
jar tf lib/jetty-servlet-api-4.0.9.jar | grep '^javax/servlet/resources/web-app_4_0.xsd$' >/dev/null 2>&1 || \
fail "Jetty Servlet API is missing the EE8 web.xml schema"
for pattern in \ for pattern in \
'jetty-deploy-*.jar' \ 'jetty-deploy-*.jar' \
'jetty-jmx-*.jar' \ 'jetty-jmx-*.jar' \
'solr-core-*.jar' \ 'solr-core-*.jar' \
'solr-solrj-*.jar' \ 'solr-solrj-*.jar' \
'solr-scripting-*.jar' \ 'solr-scripting-*.jar' \
'http2-*.jar'; do 'http2-*.jar' \
'slf4j-*-1.7.36.jar'; do
for artifact in lib/$pattern; do for artifact in lib/$pattern; do
[ -e "$artifact" ] || continue [ -e "$artifact" ] || continue
fail "forbidden resolved artifact: $artifact" fail "forbidden resolved artifact: $artifact"
@@ -66,7 +124,9 @@ if [ -d lib ]; then
jetty-util-9.4.58.v20250814 \ jetty-util-9.4.58.v20250814 \
http2-client-9.4.58.v20250814 \ http2-client-9.4.58.v20250814 \
http2-common-9.4.58.v20250814 \ http2-common-9.4.58.v20250814 \
http2-http-client-transport-9.4.58.v20250814; do http2-http-client-transport-9.4.58.v20250814 \
slf4j-api-1.7.36 \
slf4j-jdk14-1.7.36; do
jar="lib/solr9-bridge-$artifact.jar" jar="lib/solr9-bridge-$artifact.jar"
[ -f "$jar" ] || fail "missing generated bridge artifact: $jar" [ -f "$jar" ] || fail "missing generated bridge artifact: $jar"
if jar tf "$jar" | grep '^org/eclipse/jetty/' >/dev/null 2>&1; then if jar tf "$jar" | grep '^org/eclipse/jetty/' >/dev/null 2>&1; then
@@ -75,7 +135,13 @@ if [ -d lib ]; then
if zipgrep -a -E 'org(/|\.)eclipse(/|\.)jetty' "$jar" >/dev/null 2>&1; then if zipgrep -a -E 'org(/|\.)eclipse(/|\.)jetty' "$jar" >/dev/null 2>&1; then
fail "unrelocated Jetty reference in $jar" fail "unrelocated Jetty reference in $jar"
fi fi
if jar tf "$jar" | grep '^org/slf4j/' >/dev/null 2>&1; then
fail "unrelocated SLF4J class in $jar"
fi
if zipgrep -a -E 'org(/|\.)slf4j' "$jar" >/dev/null 2>&1; then
fail "unrelocated SLF4J reference in $jar"
fi
done done
fi fi
echo "PASS: Solr 9 uses only the relocated Jetty client island." echo "PASS: Solr 9 uses only the relocated Jetty client and SLF4J 1.7 island."
+49 -16
View File
@@ -1,9 +1,10 @@
/* /*
* Proof-of-concept bytecode relocator for the Jetty 12 migration. * Build-time bytecode relocator for the Jetty 12 migration.
* *
* This is deliberately a small build tool, not YaCy runtime code. It keeps * 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 * Solr's public packages unchanged while moving its private Jetty 9 linkage
* below net.yacy.solr9.jetty. * below net.yacy.solr9.jetty and its SLF4J 1.7 linkage below
* net.yacy.solr9.slf4j.
*/ */
package net.yacy.test.jetty; package net.yacy.test.jetty;
@@ -26,10 +27,12 @@ import org.objectweb.asm.commons.Remapper;
public final class RelocateJettyPackages { public final class RelocateJettyPackages {
private static final String SOURCE_INTERNAL = "org/eclipse/jetty"; private static final Relocation[] RELOCATIONS = {
private static final String TARGET_INTERNAL = "net/yacy/solr9/jetty"; new Relocation("org/eclipse/jetty", "net/yacy/solr9/jetty",
private static final String SOURCE_BINARY = "org.eclipse.jetty"; "org.eclipse.jetty", "net.yacy.solr9.jetty"),
private static final String TARGET_BINARY = "net.yacy.solr9.jetty"; new Relocation("org/slf4j", "net/yacy/solr9/slf4j",
"org.slf4j", "net.yacy.solr9.slf4j")
};
private RelocateJettyPackages() { private RelocateJettyPackages() {
} }
@@ -43,7 +46,7 @@ public final class RelocateJettyPackages {
private static void relocate(final Path input, final Path output) throws IOException { private static void relocate(final Path input, final Path output) throws IOException {
Files.createDirectories(output.toAbsolutePath().getParent()); Files.createDirectories(output.toAbsolutePath().getParent());
final Remapper remapper = new JettyRemapper(); final Remapper remapper = new BridgeRemapper();
try (JarFile source = new JarFile(input.toFile()); try (JarFile source = new JarFile(input.toFile());
JarOutputStream target = new JarOutputStream(Files.newOutputStream(output))) { JarOutputStream target = new JarOutputStream(Files.newOutputStream(output))) {
@@ -62,8 +65,7 @@ public final class RelocateJettyPackages {
if (entry.getName().endsWith(".class")) { if (entry.getName().endsWith(".class")) {
target.write(relocateClass(content, remapper)); target.write(relocateClass(content, remapper));
} else if (entry.getName().startsWith("META-INF/services/")) { } else if (entry.getName().startsWith("META-INF/services/")) {
target.write(new String(content, StandardCharsets.UTF_8) target.write(relocateText(new String(content, StandardCharsets.UTF_8))
.replace(SOURCE_BINARY, TARGET_BINARY)
.getBytes(StandardCharsets.UTF_8)); .getBytes(StandardCharsets.UTF_8));
} else { } else {
target.write(content); target.write(content);
@@ -74,28 +76,42 @@ public final class RelocateJettyPackages {
} }
} }
private static final class JettyRemapper extends Remapper { private static final class BridgeRemapper extends Remapper {
private JettyRemapper() { private BridgeRemapper() {
super(Opcodes.ASM9); super(Opcodes.ASM9);
} }
@Override @Override
public String map(final String internalName) { public String map(final String internalName) {
return replacePrefix(internalName, SOURCE_INTERNAL, TARGET_INTERNAL); return relocateInternalName(internalName);
} }
@Override @Override
public Object mapValue(final Object value) { public Object mapValue(final Object value) {
if (value instanceof String) { if (value instanceof String) {
final String text = (String) value; final String text = (String) value;
return text.replace(SOURCE_BINARY, TARGET_BINARY) return relocateText(text);
.replace(SOURCE_INTERNAL, TARGET_INTERNAL);
} }
return super.mapValue(value); return super.mapValue(value);
} }
} }
private static final class Relocation {
private final String sourceInternal;
private final String targetInternal;
private final String sourceBinary;
private final String targetBinary;
private Relocation(final String sourceInternal, final String targetInternal,
final String sourceBinary, final String targetBinary) {
this.sourceInternal = sourceInternal;
this.targetInternal = targetInternal;
this.sourceBinary = sourceBinary;
this.targetBinary = targetBinary;
}
}
private static byte[] relocateClass(final byte[] content, final Remapper remapper) { private static byte[] relocateClass(final byte[] content, final Remapper remapper) {
final ClassReader reader = new ClassReader(content); final ClassReader reader = new ClassReader(content);
final ClassWriter writer = new ClassWriter(0); final ClassWriter writer = new ClassWriter(0);
@@ -120,13 +136,30 @@ public final class RelocateJettyPackages {
} }
private static String relocateEntryName(final String name) { private static String relocateEntryName(final String name) {
final String internalName = replacePrefix(name, SOURCE_INTERNAL, TARGET_INTERNAL); final String internalName = relocateInternalName(name);
if (internalName.startsWith("META-INF/services/")) { if (internalName.startsWith("META-INF/services/")) {
return internalName.replace(SOURCE_BINARY, TARGET_BINARY); return relocateText(internalName);
} }
return internalName; return internalName;
} }
private static String relocateInternalName(final String name) {
String relocated = name;
for (final Relocation relocation : RELOCATIONS) {
relocated = replacePrefix(relocated, relocation.sourceInternal, relocation.targetInternal);
}
return relocated;
}
private static String relocateText(final String text) {
String relocated = text;
for (final Relocation relocation : RELOCATIONS) {
relocated = relocated.replace(relocation.sourceBinary, relocation.targetBinary)
.replace(relocation.sourceInternal, relocation.targetInternal);
}
return relocated;
}
private static byte[] readAll(final InputStream stream) throws IOException { private static byte[] readAll(final InputStream stream) throws IOException {
final ByteArrayOutputStream output = new ByteArrayOutputStream(); final ByteArrayOutputStream output = new ByteArrayOutputStream();
stream.transferTo(output); stream.transferTo(output);
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env sh
# Live HTTP proxy and CONNECT contract. The peer must be started with the
# transparent proxy enabled and both targets must be controlled test services.
set -eu
proxy=${YACY_SMOKE_PROXY:-}
http_target=${YACY_SMOKE_PROXY_HTTP_TARGET:-}
https_target=${YACY_SMOKE_PROXY_HTTPS_TARGET:-}
[ -n "$proxy" ] && [ -n "$http_target" ] && [ -n "$https_target" ] || {
echo "SKIP: set YACY_SMOKE_PROXY, YACY_SMOKE_PROXY_HTTP_TARGET and YACY_SMOKE_PROXY_HTTPS_TARGET" >&2
exit 2
}
timeout=${YACY_SMOKE_TIMEOUT:-30}
curl --silent --show-error --fail --max-time "$timeout" --proxy "$proxy" \
--noproxy '' \
"$http_target" >/dev/null
echo "ok 1 - HTTP proxy traffic"
# An HTTPS request through an HTTP proxy necessarily establishes a CONNECT tunnel.
curl --silent --show-error --fail --max-time "$timeout" --proxy "$proxy" \
--noproxy '' \
"$https_target" >/dev/null
echo "ok 2 - HTTPS CONNECT tunnel"
echo "PASS: proxy and CONNECT contract"
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env sh
# Exercises YaCy's Apache-HttpClient-backed RemoteInstance against an external
# Solr collection. This is an opt-in integration gate.
set -eu
[ -n "${YACY_REMOTE_SOLR_URL:-}" ] || {
echo "SKIP: set YACY_REMOTE_SOLR_URL (for example http://host:8983/solr/)" >&2
exit 2
}
repo_root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
cd "$repo_root"
[ -d build/classes/java/main ] || { echo "FAIL: run ant compile first" >&2; exit 1; }
work=$(mktemp -d "${TMPDIR:-/tmp}/yacy-remote-solr-smoke.XXXXXX")
trap 'rm -rf "$work"' EXIT HUP INT TERM
classpath=build/classes/java/main
for archive in lib/*.jar; do
case $(basename "$archive") in yacycore.jar) continue ;; esac
classpath="$classpath:$archive"
done
javac --release 17 -cp "$classpath" -d "$work" \
test/java/net/yacy/cora/federate/solr/connector/RemoteSolrSmoke.java
java -cp "$work:$classpath" net.yacy.cora.federate.solr.connector.RemoteSolrSmoke \
"$YACY_REMOTE_SOLR_URL"
echo "PASS: remote Solr request through YaCy RemoteInstance"