isolate Solr 9.0 Jetty client dependencies in a relocated bridge

This commit is contained in:
Michael Peter Christen committed 2026-07-11 23:40:15 +02:00
1 parent edeb6162c4
commit bfddfe5e7e
20 files changed
+1118 -152

No files matched your search

+10
View File
@@ -12,5 +12,15 @@
<attribute name="module" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="lib" path="lib/solr9-bridge-solr-core-9.0.0.jar"/>
<classpathentry kind="lib" path="lib/solr9-bridge-solr-solrj-9.0.0.jar"/>
<classpathentry kind="lib" path="lib/solr9-bridge-solr-scripting-9.0.0.jar"/>
<classpathentry kind="lib" path="lib/solr9-bridge-jetty-client-9.4.58.v20250814.jar"/>
<classpathentry kind="lib" path="lib/solr9-bridge-jetty-http-9.4.58.v20250814.jar"/>
<classpathentry kind="lib" path="lib/solr9-bridge-jetty-io-9.4.58.v20250814.jar"/>
<classpathentry kind="lib" path="lib/solr9-bridge-jetty-util-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-http-client-transport-9.4.58.v20250814.jar"/>
<classpathentry kind="output" path="gen"/>
</classpath>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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_MANUAL_TARGETS" value="build-solr9-bridge,"/>
<booleanAttribute key="org.eclipse.ui.externaltools.ATTR_TRIGGERS_CONFIGURED" value="true"/>
<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}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_WORKING_DIRECTORY" value="${workspace_loc:/yacy}"/>
</launchConfiguration>
+10
View File
@@ -10,6 +10,16 @@
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.ui.externaltools.ExternalToolBuilder</name>
<triggers>full,</triggers>
<arguments>
<dictionary>
<key>LaunchConfigHandle</key>
<value>&lt;project&gt;/.externalToolBuilders/solr9-bridge.launch</value>
</dictionary>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
+203
View File
@@ -0,0 +1,203 @@
# Jetty 12 Migration Contract
## Scope
YaCy will migrate its embedded HTTP server from Jetty 9 to Jetty 12 while
remaining on Java 17 and keeping the existing Servlet 4.0 `javax.servlet`
source API. Jetty 12's EE8 environment provides this compatibility layer; the
migration does not require converting YaCy to `jakarta.servlet`.
This contract covers the dependency boundary only. It does not yet migrate the
Jetty-specific adapters under `source/net/yacy/http`.
## Solr Boundary
YaCy embeds Solr 9 but does not directly use Solr's embedded `JettySolrRunner`
or its Jetty-based HTTP/2 clients. Remote Solr access is implemented with
`HttpSolrClient` and `ConcurrentUpdateSolrClient`, both configured with Apache
HttpClient in `RemoteInstance`.
The three Solr artifacts in `ivy.xml` therefore belong to the private
`solr9-bridge` configuration and explicitly exclude the `org.eclipse.jetty`
and `org.eclipse.jetty.http2` families. They are retrieved below
`build/solr9-bridge/input`, not directly into the runtime `lib` directory.
Solr 9.0 nevertheless has an eager internal dependency that cannot be removed:
`CoreContainer` constructs both `HttpShardHandlerFactory` and
`UpdateShardHandler`, which create `Http2SolrClient` instances during startup
even in YaCy's standalone embedded configuration. The build therefore rewrites
Solr's Jetty references and the minimal Jetty 9 client/HTTP2 implementation to
the private `net.yacy.solr9.jetty` package. The generated artifacts have the
`solr9-bridge-` prefix under `lib/`; unrelocated Solr and HTTP2 jars are removed
from that directory before compilation.
Run the boundary guard after resolving dependencies:
```sh
ant clean compile
test/jetty-solr-dependency-guard.sh
```
## Current Jetty 9 Roots
The direct Jetty dependencies reflect APIs imported by YaCy source code.
Dependencies needed only by another Jetty module remain transitive.
| Responsibility | Direct Jetty 9 artifact |
| --- | --- |
| HTTP primitives | `jetty-http` |
| Connection and output APIs | `jetty-io` |
| Server and handlers | `jetty-server` |
| Utility, resource, and TLS APIs | `jetty-util` |
| CONNECT and proxy handlers | `jetty-proxy` |
| Authentication and constraints | `jetty-security` |
| Servlet container | `jetty-servlet` |
| QoS and servlet helpers | `jetty-servlets` |
| Web application context | `jetty-webapp` |
The public `jetty-client` is owned transitively by `jetty-proxy`. YaCy has no
direct client API imports. `jetty-deploy` and `jetty-jmx` are not part of the
current root graph. HTTP/2 exists only inside the relocated Solr island, not as
an embedded-server feature.
## Jetty 12 EE8 Target Graph
The minimal target keeps Jetty Core separate from the EE8 Servlet layer.
| Current responsibility | Jetty 12 target family |
| --- | --- |
| HTTP, IO, server, utilities | Jetty Core `jetty-http`, `jetty-io`, `jetty-server`, `jetty-util` |
| Proxy handlers | Jetty Core `jetty-proxy` and its client transitive |
| Servlet container | `org.eclipse.jetty.ee8:jetty-ee8-servlet` |
| Servlet helpers and QoS | `org.eclipse.jetty.ee8:jetty-ee8-servlets` |
| Web application context | `org.eclipse.jetty.ee8:jetty-ee8-webapp` |
| Servlet authentication | `org.eclipse.jetty.ee8:jetty-ee8-security` plus Jetty Core security transitives |
| Servlet API | Servlet 4.0 in the `javax.servlet` namespace, aligned with the EE8 environment |
Do not add Jetty deploy, JMX, server-side HTTP/2, or Jakarta EE modules unless
a concrete YaCy feature requires them and its contract is verified separately.
The private Solr 9 HTTP/2 island above is the only current exception.
### Solr 9 Isolation Gate (P1)
Jetty 9 client classes and Jetty 12 server classes cannot safely share the
same application classloader because they use overlapping
`org.eclipse.jetty.*` packages with incompatible APIs.
The P1 investigation rules out the two initially attractive shortcuts:
1. **A Solr upgrade is not part of this migration.** YaCy remains on Solr
9.0.0. A later Solr upgrade may provide another migration option, but it is
deliberately not a P1 implementation path. A comparison with Solr 9.10.1
also showed that upgrading within the Solr 9 line would not remove the
boundary: it selects Jetty 10.0.26 and still eagerly constructs
`Http2SolrClient` instances.
2. **Whole-Solr classloader isolation is not a minimal boundary for YaCy.**
Solr API objects are part of the application boundary: 88 files under
`source/` and `test/` currently import `org.apache.solr` types. A child
classloader would either create incompatible class identities or require a
broad new facade and data conversion layer.
Solr 10 is also outside this migration contract: although it moves to Jetty
12, the Solr 10 server requires Java 21 and uses the Jakarta Servlet namespace.
P1 implements a **relocated Solr 9.0.0 Jetty client bridge**. The bridge keeps
the public `org.apache.solr.*` classes visible to YaCy while
rewriting Solr 9.0.0's internal `org.eclipse.jetty.*` references and the
required Jetty 9 client/HTTP2 implementation into a private package. It is
built reproducibly by Ant; no edited jar is stored in the repository. P1 does
not change the Solr or Lucene versions.
P1 is complete only when a bridge proof passes all of these checks:
1. only the selected embedded-server Jetty line uses the public
`org.eclipse.jetty` package;
2. no unrelocated Jetty 9/10 class is packaged by the bridge;
3. `EmbeddedSolrConnectorTest` starts and closes a `CoreContainer`;
4. an embedded update followed by a query succeeds;
5. the bridge dependency set and relocation rules are generated by the build;
6. the bridge can be removed without changing YaCy's Solr-facing source API.
The integrated bridge passes `EmbeddedSolrConnectorTest` (`OK (4 tests)`) with
the original Solr and HTTP2 jars removed. It also passes when
Jetty 12.1.11 Core client, HTTP, IO, proxy, security, server, and utility jars
are present in the same application classpath. The reduced private island is:
- Solr Core, SolrJ, and Solr Scripting 9.0.0 with only their Jetty references
rewritten;
- Jetty 9.4 client, HTTP, IO, and utility;
- Jetty 9.4 HTTP/2 client, common, and HTTP-client transport.
Jetty server, servlet, security, proxy, webapp, and XML are not included in
the Solr island. The proof can be repeated with:
```sh
ant clean compile
test/solr9-jetty-bridge-spike.sh
```
Set `JETTY12_CLASSPATH` to a colon-separated set of resolved Jetty 12 jars to
repeat the coexistence variant. The script compiles only its focused test into
a temporary directory; the bridge itself is already produced by `ant compile`.
All six P1 checks pass. The Solr isolation gate is therefore closed for the
Jetty 12 server migration. Reconsidering Solr remains a separate future
decision, not an automatic part of this migration.
The optional version comparison that established the limitation of the Solr 9
line can be repeated without changing YaCy's production dependencies with:
```sh
javap -classpath ~/.ivy2/cache/org.apache.solr/solr-core/jars/solr-core-9.10.1.jar \
-private -c org.apache.solr.update.UpdateShardHandler
javap -classpath ~/.ivy2/cache/org.apache.solr/solr-core/jars/solr-core-9.10.1.jar \
-private -c org.apache.solr.handler.component.HttpShardHandlerFactory
```
## Completion Gates
The dependency phase is complete when all of these checks pass with one public
Jetty version on the resolved classpath and the private Solr island:
1. `ant clean compile`
2. `test/jetty-solr-dependency-guard.sh`
3. Startup with embedded Solr enabled
4. A proven resolution for the Solr 9 isolation gate
5. A remote Solr request through the Apache-based client
6. Proxy traffic including CONNECT
7. `test/jetty-smoke-test.sh`
The following implementation phase may then replace `Jetty9HttpServerImpl` and
the remaining Jetty adapter APIs without changing the Solr dependency graph.
## P2.1 Helper Removal
`YaCyDefaultServlet` no longer imports Jetty HTTP header/method constants,
MIME lookup, URI joining, writer adaptation, inclusive byte ranges, multipart
output, or `Resource`. The small operations use Servlet/JDK APIs or local
implementations. Static resources are exposed through the container-neutral
`ServletResource` interface; `Jetty9ServletResource` is the only Jetty 9
adapter for the existing resource behavior.
`YaCyQoSFilter` and `YaCyDigestCredential` remain explicit container adapters
rather than being replaced by simplified local implementations that could
change request priority or authentication behavior.
## P2.2 Authentication And Access Rules
The request-level administrator decision is represented by
`AdminAccessPolicy`: public access, the configured localhost bypass, or the
administrator role. It combines the existing pure `AdminSecurity` checks
without depending on Servlet or Jetty APIs. In particular, the localhost
without account option and the localhost-only stored-hash authentication used
by `bin/apicall.sh` remain supported.
`AdminAuthenticationContext` carries the true socket peer IP only for the
duration of the current authentication call. `YaCySecurityHandler` publishes
and clears that context, and `YaCyDigestCredential` only adapts Jetty's BASIC
and DIGEST credential objects to the container-neutral password check.
The portable address/path syntax of `serverClient` is represented by
`InetPathAccessRule`. `InetPathAccessHandler` remains the Jetty 9 matcher
adapter; Jetty 12 can consume the normalized `address|path` rules with its
native path-aware access handler.
+35 -1
View File
@@ -38,6 +38,10 @@
<property name="lib" location="lib"/>
<property name="libt" location="libt"/>
<property name="build" location="build/classes/java/main"/> <!-- reuse Gradle build path -->
<property name="solr9.bridge.build" location="build/solr9-bridge"/>
<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.libs" location="${solr9.bridge.build}/tool-libs"/>
<property name="javadoc" location="javadoc"/>
<property name="htroot" location="htroot"/>
<property name="test" location="test/java"/>
@@ -91,6 +95,8 @@
-->
<ivy:retrieve conf="compile" pathid="compile.path" pattern="${ivy.lib.dir}/[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-tool" pathid="solr9.bridge.tool.path" pattern="${solr9.bridge.tool.libs}/[artifact]-[revision].[ext]" />
<property name="target-resolve-already-run" value="true" />
</target>
@@ -171,9 +177,36 @@
<pathelement location="${build}" />
<pathelement location="${lib}/J7Zip-modified-1.0.2.jar" />
<path refid="compile.path" />
<fileset dir="${lib}" includes="solr9-bridge-*.jar" />
</path>
<target name="compile-core" depends="init,resolve" description="compile YaCy core">
<target name="build-solr9-bridge" depends="init,resolve" description="build the private Solr 9.0 Jetty client island">
<delete dir="${solr9.bridge.tool}" 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" />
</delete>
<mkdir dir="${solr9.bridge.tool}"/>
<javac srcdir="test/jetty" destdir="${solr9.bridge.tool}"
includes="RelocateJettyPackages.java" includeantruntime="false"
release="${javacRelease}" encoding="UTF-8">
<classpath refid="solr9.bridge.tool.path"/>
</javac>
<path id="solr9.bridge.runtime.path">
<pathelement location="${solr9.bridge.tool}"/>
<path refid="solr9.bridge.tool.path"/>
</path>
<apply executable="${java.home}/bin/java" parallel="false" failonerror="true" dest="${lib}">
<arg value="-cp"/>
<arg pathref="solr9.bridge.runtime.path"/>
<arg value="net.yacy.test.jetty.RelocateJettyPackages"/>
<srcfile/>
<targetfile/>
<fileset dir="${solr9.bridge.input}" includes="*.jar"/>
<mapper type="glob" from="*" to="solr9-bridge-*"/>
</apply>
</target>
<target name="compile-core" depends="init,resolve,build-solr9-bridge" description="compile YaCy core">
<!-- compile the core sources -->
<echo message="project.class.path: ${toString:project.class.path}" />
<javac srcdir="${src}/" destdir="${build}"
@@ -417,6 +450,7 @@
<fileset dir="." includes="TEST-*" />
</delete>
<delete dir="test/DATA" failonerror="false"/>
<delete dir="${solr9.bridge.build}" failonerror="false"/>
</target>
<!-- run YaCy (needed for NetBeans4) -->
+36 -11
View File
@@ -4,6 +4,8 @@
<!-- https://wrongnotes.blogspot.com/2014/02/simplest-explanation-of-ivy.html -->
<conf name="compile" visibility="public"/>
<conf name="test" visibility="public"/>
<conf name="solr9-bridge" visibility="private"/>
<conf name="solr9-bridge-tool" visibility="private"/>
</configurations>
<dependencies defaultconf="compile->default">
@@ -64,17 +66,31 @@
<dependency org="org.apache.poi" name="poi" rev="5.5.1" />
<dependency org="org.apache.poi" name="poi-scratchpad" rev="5.5.1" />
<dependency org="org.apache.solr" name="solr-core" rev="9.0.0" conf="compile->master"/>
<dependency org="org.apache.solr" name="solr-scripting" rev="9.0.0" conf="compile->master"/>
<dependency org="org.apache.solr" name="solr-solrj" rev="9.0.0" conf="compile->master" />
<!--
Solr is embedded and its Jetty-backed runner and HTTP/2 clients are not used directly by YaCy.
Keep Solr's Jetty 9 runtime family out of the embedded-server classpath so Jetty can
be migrated independently. The master configuration already omits transitives; the
explicit excludes preserve that boundary if the configuration mapping changes later.
-->
<dependency org="org.apache.solr" name="solr-core" rev="9.0.0" conf="solr9-bridge->master">
<exclude org="org.eclipse.jetty" module="*"/>
<exclude org="org.eclipse.jetty.http2" module="*"/>
</dependency>
<dependency org="org.apache.solr" name="solr-scripting" rev="9.0.0" conf="solr9-bridge->master">
<exclude org="org.eclipse.jetty" module="*"/>
<exclude org="org.eclipse.jetty.http2" module="*"/>
</dependency>
<dependency org="org.apache.solr" name="solr-solrj" rev="9.0.0" conf="solr9-bridge->master">
<exclude org="org.eclipse.jetty" module="*"/>
<exclude org="org.eclipse.jetty.http2" module="*"/>
</dependency>
<dependency org="org.apache.zookeeper" name="zookeeper" rev="3.9.5" conf="compile->master" />
<dependency org="org.bitlet" name="weupnp" rev="0.1.4" />
<dependency org="org.bouncycastle" name="bcmail-jdk18on" rev="1.84" />
<dependency org="com.fasterxml.woodstox" name="woodstox-core" rev="7.2.1" />
<dependency org="org.eclipse.jetty" name="jetty-client" rev="9.4.58.v20250814" />
<dependency org="org.eclipse.jetty" name="jetty-deploy" rev="9.4.58.v20250814" conf="compile->master" />
<dependency org="org.eclipse.jetty" name="jetty-jmx" rev="9.4.58.v20250814" conf="compile->master"/>
<dependency org="org.eclipse.jetty" name="jetty-http" rev="9.4.58.v20250814"/>
<dependency org="org.eclipse.jetty" name="jetty-http" rev="9.4.58.v20250814" conf="compile->default;solr9-bridge->master"/>
<dependency org="org.eclipse.jetty" name="jetty-io" rev="9.4.58.v20250814" conf="compile->default;solr9-bridge->master"/>
<dependency org="org.eclipse.jetty" name="jetty-client" rev="9.4.58.v20250814" conf="solr9-bridge->master"/>
<dependency org="org.eclipse.jetty" name="jetty-proxy" rev="9.4.58.v20250814"/>
<dependency org="org.eclipse.jetty" name="jetty-security" rev="9.4.58.v20250814"/>
<dependency org="org.eclipse.jetty" name="jetty-server" rev="9.4.58.v20250814"/>
@@ -82,11 +98,17 @@
<dependency org="org.eclipse.jetty" name="jetty-servlet" rev="9.4.58.v20250814">
<exclude module="jetty-util-ajax" />
</dependency>
<dependency org="org.eclipse.jetty" name="jetty-util" rev="9.4.58.v20250814" />
<dependency org="org.eclipse.jetty" name="jetty-util" rev="9.4.58.v20250814" conf="compile->default;solr9-bridge->master" />
<dependency org="org.eclipse.jetty" name="jetty-webapp" rev="9.4.58.v20250814" />
<dependency org="org.eclipse.jetty.http2" name="http2-client" rev="9.4.58.v20250814" conf="compile->master"/>
<dependency org="org.eclipse.jetty.http2" name="http2-common" rev="9.4.58.v20250814" conf="compile->master"/>
<dependency org="org.eclipse.jetty.http2" name="http2-http-client-transport" rev="9.4.58.v20250814" conf="compile->master"/>
<!--
Temporary Solr 9.0 runtime bridge: CoreContainer eagerly constructs an
Http2SolrClient for its shard/update handlers even in standalone mode.
YaCy source must not use these clients. Remove this Jetty 9 island only
after the relocated Solr 9.0 bridge replaces these unrelocated jars.
-->
<dependency org="org.eclipse.jetty.http2" name="http2-client" rev="9.4.58.v20250814" conf="solr9-bridge->master"/>
<dependency org="org.eclipse.jetty.http2" name="http2-common" rev="9.4.58.v20250814" conf="solr9-bridge->master"/>
<dependency org="org.eclipse.jetty.http2" name="http2-http-client-transport" rev="9.4.58.v20250814" conf="solr9-bridge->master"/>
<dependency org="org.jsoup" name="jsoup" rev="1.22.2" />
<dependency org="org.jwat" name="jwat-warc" rev="1.2.1">
<exclude module="bcprov-jdk15to18" />
@@ -104,6 +126,9 @@
<dependency org="junit" name="junit" rev="4.13.2" conf="test->default"/>
<dependency org="org.hamcrest" name="hamcrest-core" rev="3.0" conf="test->default"/>
<!-- Build-time bytecode relocation for the private Solr 9.0 Jetty client island. -->
<dependency org="org.ow2.asm" name="asm" 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="ai.djl" name ="api" rev="0.36.0"></dependency>
</dependencies>
@@ -0,0 +1,60 @@
package net.yacy.http;
import java.net.MalformedURLException;
import net.yacy.cora.document.id.MultiProtocolURL;
/** Container-neutral policy for administrator access to a request path. */
public final class AdminAccessPolicy {
public enum Decision {
PUBLIC,
LOCAL_BYPASS,
ADMIN_REQUIRED
}
private final boolean protectAllPages;
private final boolean privateRobinsonMode;
private final boolean publicSearchPage;
private final boolean allowLocalhostWithoutLogin;
private final String adminUser;
private final String adminHash;
public AdminAccessPolicy(final boolean protectAllPages, final boolean privateRobinsonMode,
final boolean publicSearchPage, final boolean allowLocalhostWithoutLogin,
final String adminUser, final String adminHash) {
this.protectAllPages = protectAllPages;
this.privateRobinsonMode = privateRobinsonMode;
this.publicSearchPage = publicSearchPage;
this.allowLocalhostWithoutLogin = allowLocalhostWithoutLogin;
this.adminUser = adminUser;
this.adminHash = adminHash;
}
public Decision decide(final String path, final String socketPeerIp, final String referer,
final String authorizationHeader) {
if (!AdminSecurity.isProtectedPath(path, this.protectAllPages,
this.privateRobinsonMode, this.publicSearchPage)) {
return Decision.PUBLIC;
}
if (AdminSecurity.isLocalhostAccess(socketPeerIp, refererHost(referer))) {
if (this.allowLocalhostWithoutLogin || AdminSecurity.checkLocalhostLazyAuth(
authorizationHeader, this.adminUser, this.adminHash)) {
return Decision.LOCAL_BYPASS;
}
}
return Decision.ADMIN_REQUIRED;
}
private static String refererHost(final String referer) {
if (referer == null || referer.isEmpty()) {
return null;
}
try {
return new MultiProtocolURL(referer).getHost();
} catch (final MalformedURLException e) {
return null;
}
}
}
@@ -0,0 +1,25 @@
package net.yacy.http;
import net.yacy.cora.protocol.Domains;
/** Request-bound facts needed while the container verifies admin credentials. */
public final class AdminAuthenticationContext {
private static final ThreadLocal<String> SOCKET_PEER_IP = new ThreadLocal<>();
private AdminAuthenticationContext() {
}
public static void setSocketPeerIp(final String ip) {
SOCKET_PEER_IP.set(ip);
}
public static void clear() {
SOCKET_PEER_IP.remove();
}
public static boolean isLocalhostRequest() {
final String ip = SOCKET_PEER_IP.get();
return ip != null && Domains.isLocalhost(ip);
}
}
@@ -98,23 +98,14 @@ public class InetPathAccessHandler extends InetAccessHandler {
*/
protected void addPattern(final String pattern, final PathMappings<InetAddressSet> pathMappings)
throws IllegalArgumentException {
if (pattern != null && !pattern.isEmpty()) {
final int idx = pattern.indexOf('|');
final String addr = idx > 0 ? pattern.substring(0, idx) : pattern;
final String path = (idx > 0 && (pattern.length() > idx + 1)) ? pattern.substring(idx + 1) : "/*";
if (!addr.isEmpty()) {
final PathSpec pathSpec = PathSpec.from(path);
InetAddressSet addresses = pathMappings.get(pathSpec);
if (addresses == null) {
addresses = new InetAddressSet();
pathMappings.put(pathSpec, addresses);
}
addresses.add(addr);
}
final InetPathAccessRule rule = InetPathAccessRule.parse(pattern);
final PathSpec pathSpec = PathSpec.from(rule.pathPattern());
InetAddressSet addresses = pathMappings.get(pathSpec);
if (addresses == null) {
addresses = new InetAddressSet();
pathMappings.put(pathSpec, addresses);
}
addresses.add(rule.addressPattern());
}
/**
@@ -0,0 +1,42 @@
package net.yacy.http;
/** Container-neutral representation of a server-client address/path rule. */
public final class InetPathAccessRule {
private static final String DEFAULT_PATH = "/*";
private final String addressPattern;
private final String pathPattern;
private InetPathAccessRule(final String addressPattern, final String pathPattern) {
this.addressPattern = addressPattern;
this.pathPattern = pathPattern;
}
public static InetPathAccessRule parse(final String pattern) {
if (pattern == null || pattern.isEmpty()) {
throw new IllegalArgumentException("Access rule must not be empty");
}
final int separator = pattern.indexOf('|');
final String address = separator > 0 ? pattern.substring(0, separator) : pattern;
final String path = separator > 0 && pattern.length() > separator + 1
? pattern.substring(separator + 1)
: DEFAULT_PATH;
if (address.isEmpty()) {
throw new IllegalArgumentException("Access rule has no address: " + pattern);
}
return new InetPathAccessRule(address, path);
}
public String addressPattern() {
return this.addressPattern;
}
public String pathPattern() {
return this.pathPattern;
}
public String asJettyPattern() {
return this.addressPattern + '|' + this.pathPattern;
}
}
+2 -43
View File
@@ -24,7 +24,6 @@
package net.yacy.http;
import net.yacy.cora.protocol.Domains;
import net.yacy.search.Switchboard;
import net.yacy.search.SwitchboardConstants;
@@ -46,43 +45,6 @@ public class YaCyDigestCredential extends Credential {
private static final long serialVersionUID = -3527894085562480001L;
/**
* True socket peer IP of the request currently being authenticated on this thread.
* <p>
* Jetty's {@link Credential#check(Object)} API does not hand over the request, so the
* credential can not tell on its own whether a request comes from localhost. The
* {@link YaCySecurityHandler} therefore publishes the request's socket peer IP here for
* the duration of the request (set at the start of handling, cleared in a finally block).
* This replaces the former global "recent localhost access" timestamp, which was a
* process-wide value not bound to the request being checked.
*/
private static final ThreadLocal<String> REQUEST_CLIENT_IP = new ThreadLocal<String>();
/**
* Publish the socket peer IP of the request being authenticated on the current thread.
* Must be paired with {@link #clearRequestClientIP()} in a finally block.
* @param ip the true socket peer IP (never an X-Real-IP derived address)
*/
public static void setRequestClientIP(final String ip) {
REQUEST_CLIENT_IP.set(ip);
}
/**
* Remove the request client IP published for the current thread.
*/
public static void clearRequestClientIP() {
REQUEST_CLIENT_IP.remove();
}
/**
* @return true when the request currently authenticated on this thread comes from
* localhost. Fails closed (returns false) when no request IP was published.
*/
private static boolean isRequestFromLocalhost() {
final String ip = REQUEST_CLIENT_IP.get();
return ip != null && Domains.isLocalhost(ip);
}
private String hash; // remember password hash, either MD5(Base64(user:pwd)) or with encryption prefix "MD5:" + MD5(user:realm:pwd)
private String foruser; // remember the user as YaCy credential is username:pwd (not just pwd)
private Credential c;
@@ -108,14 +70,11 @@ public class YaCyDigestCredential extends Credential {
//
// We must therefore know whether THIS request comes from localhost. Jetty's
// Credential.check() is not given the request, so YaCySecurityHandler publishes
// the request's true socket peer IP into REQUEST_CLIENT_IP for the duration of the
// request (see the detailed rationale on YaCySecurityHandler.handle()); we read it
// back here. Using the socket peer - not the spoofable X-Real-IP header - keeps
// this exception restricted to genuine local callers.
// the true socket peer IP through AdminAuthenticationContext for this request.
return AdminSecurity.checkAdminPassword(this.foruser, this.hash,
sb.getConfig(SwitchboardConstants.ADMIN_REALM, ""),
sb.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_USER_NAME, "admin"),
isRequestFromLocalhost(),
AdminAuthenticationContext.isLocalhostRequest(),
(String) credentials);
}
throw new UnsupportedOperationException();
+13 -25
View File
@@ -25,13 +25,11 @@
package net.yacy.http;
import java.io.IOException;
import java.net.MalformedURLException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.yacy.cora.document.id.MultiProtocolURL;
import net.yacy.cora.protocol.RequestHeader;
import net.yacy.search.Switchboard;
import net.yacy.search.SwitchboardConstants;
@@ -47,7 +45,7 @@ import org.eclipse.jetty.server.Request;
* and updates AccessTracker
*
* This is a thin adapter to the servlet container: the decision logic is in
* the container neutral {@link AdminSecurity}.
* the container-neutral {@link AdminAccessPolicy} and {@link AdminSecurity}.
*/
public class YaCySecurityHandler extends ConstraintSecurityHandler {
@@ -82,11 +80,11 @@ public class YaCySecurityHandler extends ConstraintSecurityHandler {
public void handle(final String pathInContext, final Request baseRequest,
final HttpServletRequest request, final HttpServletResponse response)
throws IOException, ServletException {
YaCyDigestCredential.setRequestClientIP(baseRequest.getRemoteAddr());
AdminAuthenticationContext.setSocketPeerIp(baseRequest.getRemoteAddr());
try {
super.handle(pathInContext, baseRequest, request, response);
} finally {
YaCyDigestCredential.clearRequestClientIP();
AdminAuthenticationContext.clear();
}
}
@@ -109,30 +107,20 @@ public class YaCySecurityHandler extends ConstraintSecurityHandler {
final String remoteip = request.getRemoteAddr();
serverAccessTracker.track(remoteip, pathInContext);
final boolean protectedPage = AdminSecurity.isProtectedPath(pathInContext,
final AdminAccessPolicy policy = new AdminAccessPolicy(
sb.getConfigBool(SwitchboardConstants.ADMIN_ACCOUNT_All_PAGES, false),
sb.isRobinsonMode() && !sb.isPublicRobinson(),
sb.getConfigBool(SwitchboardConstants.PUBLIC_SEARCHPAGE, true));
if (!protectedPage) {
sb.getConfigBool(SwitchboardConstants.PUBLIC_SEARCHPAGE, true),
sb.getConfigBool(SwitchboardConstants.ADMIN_ACCOUNT_FOR_LOCALHOST, false),
sb.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_USER_NAME, "admin"),
sb.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, ""));
final AdminAccessPolicy.Decision decision = policy.decide(pathInContext, remoteip,
request.getHeader(RequestHeader.REFERER), request.getHeader(RequestHeader.AUTHORIZATION));
if (decision == AdminAccessPolicy.Decision.PUBLIC) {
return super.prepareConstraintInfo(pathInContext, request);
}
String refererHost;
try {
refererHost = new MultiProtocolURL(request.getHeader(RequestHeader.REFERER)).getHost();
} catch (MalformedURLException e) {
refererHost = null;
}
if (AdminSecurity.isLocalhostAccess(remoteip, refererHost)) {
if (sb.getConfigBool(SwitchboardConstants.ADMIN_ACCOUNT_FOR_LOCALHOST, false)) {
return null;
}
// last chance to authorize using the admin from localhost
if (AdminSecurity.checkLocalhostLazyAuth(request.getHeader(RequestHeader.AUTHORIZATION),
sb.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_USER_NAME, "admin"),
sb.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, ""))) {
return null;
}
if (decision == AdminAccessPolicy.Decision.LOCAL_BYPASS) {
return null;
}
RoleInfo roleinfo = new RoleInfo();
roleinfo.setChecked(true); // RoleInfo.setChecked() : in Jetty this means - marked to have any security constraint
@@ -0,0 +1,99 @@
package net.yacy.http.servlets;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import org.eclipse.jetty.util.resource.Resource;
/** Jetty 9 adapter for the static-resource operations used by YaCy. */
final class Jetty9ServletResource implements ServletResource {
private final Resource delegate;
private Jetty9ServletResource(final Resource delegate) {
this.delegate = delegate;
}
static void disableDefaultCaches() {
Resource.setDefaultUseCaches(false);
}
static ServletResource from(final String location) throws IOException {
return wrap(Resource.newResource(location));
}
static ServletResource from(final File file) throws IOException {
return wrap(Resource.newResource(file));
}
static ServletResource from(final URL url) throws IOException {
return wrap(Resource.newResource(url));
}
private static ServletResource wrap(final Resource resource) {
return resource == null ? null : new Jetty9ServletResource(resource);
}
@Override
public ServletResource addPath(final String path) throws IOException {
return wrap(this.delegate.addPath(path));
}
@Override
public boolean exists() {
return this.delegate.exists();
}
@Override
public boolean isDirectory() {
return this.delegate.isDirectory();
}
@Override
public long lastModified() {
return this.delegate.lastModified();
}
@Override
public long length() {
return this.delegate.length();
}
@Override
public String getName() {
return this.delegate.getName();
}
@Override
public File getFile() throws IOException {
return this.delegate.getFile();
}
@Override
public InputStream getInputStream() throws IOException {
return this.delegate.getInputStream();
}
@Override
public String getListHTML(final String base, final boolean parent, final String query) throws IOException {
return this.delegate.getListHTML(base, parent, query);
}
@Override
public void writeTo(final OutputStream output, final long start, final long count) throws IOException {
this.delegate.writeTo(output, start, count);
}
@Override
public void close() {
this.delegate.close();
}
@Override
public String toString() {
return this.delegate.toString();
}
}
@@ -0,0 +1,33 @@
package net.yacy.http.servlets;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.File;
/** Container-neutral view of a static resource served by YaCy. */
public interface ServletResource extends AutoCloseable {
ServletResource addPath(String path) throws IOException;
boolean exists();
boolean isDirectory();
long lastModified();
long length();
String getName();
File getFile() throws IOException;
InputStream getInputStream() throws IOException;
String getListHTML(String base, boolean parent, String query) throws IOException;
void writeTo(OutputStream output, long start, long count) throws IOException;
@Override
void close();
}
@@ -28,7 +28,9 @@ import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStream;
import java.io.Writer;
import java.io.FilterOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -57,16 +59,7 @@ import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.io.WriterOutputStream;
import org.eclipse.jetty.server.InclusiveByteRange;
import org.eclipse.jetty.util.MultiPartOutputStream;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.resource.Resource;
import com.google.common.net.HttpHeaders;
import com.google.common.net.HttpHeaders;
import net.yacy.cora.date.GenericFormatter;
import net.yacy.cora.document.analysis.Classification;
@@ -127,10 +120,18 @@ public class YaCyDefaultServlet extends HttpServlet {
private static final long serialVersionUID = 4900000000000001110L;
protected ServletContext _servletContext;
protected boolean _acceptRanges = true;
protected boolean _dirAllowed = true;
protected Resource _resourceBase;
protected MimeTypes _mimeTypes;
protected String[] _welcomes;
protected boolean _dirAllowed = true;
protected ServletResource _resourceBase;
protected String[] _welcomes;
private static final String METHOD_HEAD = "HEAD";
private static final String METHOD_POST = "POST";
private static final String HEADER_CONTENT_RANGE = "Content-Range";
private static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since";
private static final String HEADER_IF_UNMODIFIED_SINCE = "If-Unmodified-Since";
private static final String HEADER_REQUEST_RANGE = "Request-Range";
private static final String MIME_TEXT_HTML = "text/html";
private static final String MIME_TEXT_HTML_UTF8 = "text/html;charset=utf-8";
protected File _htLocalePath;
protected File _htDocsPath;
@@ -149,8 +150,7 @@ public class YaCyDefaultServlet extends HttpServlet {
this._servletContext = this.getServletContext();
this._mimeTypes = new MimeTypes();
final String tmpstr = this.getServletContext().getInitParameter("welcomeFile");
final String tmpstr = this.getServletContext().getInitParameter("welcomeFile");
if (tmpstr == null) {
this._welcomes = HTTPDFileHandler.defaultFiles;
} else {
@@ -159,14 +159,14 @@ public class YaCyDefaultServlet extends HttpServlet {
this._acceptRanges = this.getInitBoolean("acceptRanges", this._acceptRanges);
this._dirAllowed = this.getInitBoolean("dirAllowed", this._dirAllowed);
Resource.setDefaultUseCaches(false); // caching is handled internally (prevent double caching)
Jetty9ServletResource.disableDefaultCaches(); // caching is handled internally (prevent double caching)
final String rb = this.getInitParameter("resourceBase");
try {
if (rb != null) {
this._resourceBase = Resource.newResource(rb);
this._resourceBase = Jetty9ServletResource.from(rb);
} else {
this._resourceBase = Resource.newResource(sb.getConfig(SwitchboardConstants.HTROOT_PATH, SwitchboardConstants.HTROOT_PATH_DEFAULT)); //default
this._resourceBase = Jetty9ServletResource.from(sb.getConfig(SwitchboardConstants.HTROOT_PATH, SwitchboardConstants.HTROOT_PATH_DEFAULT)); //default
}
} catch (final IOException e) {
ConcurrentLog.severe("FILEHANDLER", "event=http.resource subsystem=http result=missing-resource-base reason=" + e.getMessage());
@@ -201,14 +201,14 @@ public class YaCyDefaultServlet extends HttpServlet {
* @param pathInContext The path to find a resource for.
* @return The resource to serve.
*/
public Resource getResource(final String pathInContext) {
Resource r = null;
public ServletResource getResource(final String pathInContext) {
ServletResource r = null;
try {
if (this._resourceBase != null) {
r = this._resourceBase.addPath(pathInContext);
} else {
final URL u = this._servletContext.getResource(pathInContext);
r = Resource.newResource(u);
r = Jetty9ServletResource.from(u);
}
if (ConcurrentLog.isFine("FILEHANDLER")) {
@@ -249,10 +249,10 @@ public class YaCyDefaultServlet extends HttpServlet {
}
String pathInContext = pathInfo == null ? "/" : pathInfo; // this is the path of the resource in _resourceBase (= path within htroot respective htDocs)
final boolean endsWithSlash = pathInContext.endsWith(URIUtil.SLASH);
final boolean endsWithSlash = pathInContext.endsWith("/");
// Find the resource
Resource resource = null;
ServletResource resource = null;
try {
@@ -266,7 +266,7 @@ public class YaCyDefaultServlet extends HttpServlet {
hasClass = true;
} else {
final String pathofClass = pathInContext.substring(0, p) + ".class";
final Resource classresource = this._resourceBase.addPath(pathofClass);
final ServletResource classresource = this._resourceBase.addPath(pathofClass);
// Does a class resource exist?
if (classresource != null && classresource.exists() && !classresource.isDirectory()) {
hasClass = true;
@@ -281,7 +281,7 @@ public class YaCyDefaultServlet extends HttpServlet {
if (!hasClass && (resource == null || !resource.exists()) && !pathInContext.contains("..")) {
// try to get this in the alternative htDocsPath
if (resource != null) resource.close();
resource = Resource.newResource(new File(this._htDocsPath, pathInContext));
resource = Jetty9ServletResource.from(new File(this._htDocsPath, pathInContext));
}
if (ConcurrentLog.isFine("FILEHANDLER")) {
@@ -301,7 +301,7 @@ public class YaCyDefaultServlet extends HttpServlet {
if (q != null && q.length() != 0) {
pathInContext += "?" + q;
}
response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(this._servletContext.getContextPath(), pathInContext)));
response.sendRedirect(response.encodeRedirectURL(addPaths(this._servletContext.getContextPath(), pathInContext)));
} else {
if (hasClass) { // this is a YaCy servlet, handle the template
this.handleTemplate(pathInfo, request, response);
@@ -379,7 +379,7 @@ public class YaCyDefaultServlet extends HttpServlet {
private boolean shouldWrapBody(final HttpServletRequest request) {
final String method = request.getMethod();
if (method == null || !HttpMethod.POST.asString().equalsIgnoreCase(method)) {
if (method == null || !METHOD_POST.equalsIgnoreCase(method)) {
return false;
}
final String contentType = request.getContentType();
@@ -498,8 +498,8 @@ public class YaCyDefaultServlet extends HttpServlet {
return null;
}
for (final String _welcome : this._welcomes) {
final String welcome_in_context = URIUtil.addPaths(pathInContext, _welcome);
final Resource welcome = this.getResource(welcome_in_context);
final String welcome_in_context = addPaths(pathInContext, _welcome);
final ServletResource welcome = this.getResource(welcome_in_context);
if (welcome != null && welcome.exists()) {
return _welcome;
}
@@ -510,15 +510,15 @@ public class YaCyDefaultServlet extends HttpServlet {
/* Check modification date headers.
* send a 304 response instead of content if not modified since
*/
protected boolean passConditionalHeaders(final HttpServletRequest request, final HttpServletResponse response, final Resource resource)
protected boolean passConditionalHeaders(final HttpServletRequest request, final HttpServletResponse response, final ServletResource resource)
throws IOException {
try {
if (!request.getMethod().equals(HttpMethod.HEAD.asString())) {
final String ifms = request.getHeader(HttpHeader.IF_MODIFIED_SINCE.asString());
if (!request.getMethod().equals(METHOD_HEAD)) {
final String ifms = request.getHeader(HEADER_IF_MODIFIED_SINCE);
if (ifms != null) {
final long ifmsl = request.getDateHeader(HttpHeader.IF_MODIFIED_SINCE.asString());
final long ifmsl = request.getDateHeader(HEADER_IF_MODIFIED_SINCE);
if (ifmsl != -1) {
if (resource.lastModified() / 1000 <= ifmsl / 1000) {
response.reset();
@@ -530,7 +530,7 @@ public class YaCyDefaultServlet extends HttpServlet {
}
// Parse the if[un]modified dates and compare to resource
final long date = request.getDateHeader(HttpHeader.IF_UNMODIFIED_SINCE.asString());
final long date = request.getDateHeader(HEADER_IF_UNMODIFIED_SINCE);
if (date != -1) {
if (resource.lastModified() / 1000 > date / 1000) {
@@ -552,7 +552,7 @@ public class YaCyDefaultServlet extends HttpServlet {
/* ------------------------------------------------------------------- */
protected void sendDirectory(final HttpServletRequest request,
final HttpServletResponse response,
final Resource resource,
final ServletResource resource,
final String pathInContext)
throws IOException {
if (!this._dirAllowed) {
@@ -560,7 +560,7 @@ public class YaCyDefaultServlet extends HttpServlet {
return;
}
final String base = URIUtil.addEncodedPaths(request.getRequestURI(), URIUtil.SLASH);
final String base = addPaths(request.getRequestURI(), "/");
final String dir = resource.getListHTML(base, pathInContext.length() > 1, request.getQueryString());
if (dir == null) {
@@ -569,7 +569,7 @@ public class YaCyDefaultServlet extends HttpServlet {
}
final byte[] data = dir.getBytes(StandardCharsets.UTF_8);
response.setContentType(MimeTypes.Type.TEXT_HTML_UTF_8.asString());
response.setContentType(MIME_TEXT_HTML_UTF8);
response.setContentLength(data.length);
response.setHeader(HeaderFramework.CACHE_CONTROL, "no-cache, no-store");
response.setDateHeader(HeaderFramework.EXPIRES, System.currentTimeMillis() + 10000); // consider that directories are not modified that often
@@ -591,7 +591,7 @@ public class YaCyDefaultServlet extends HttpServlet {
protected void sendData(final HttpServletRequest request,
final HttpServletResponse response,
final boolean include,
final Resource resource,
final ServletResource resource,
final Enumeration<String> reqRanges)
throws IOException {
@@ -602,7 +602,7 @@ public class YaCyDefaultServlet extends HttpServlet {
try {
out = response.getOutputStream();
} catch (final IllegalStateException e) {
out = new WriterOutputStream(response.getWriter());
out = new CharacterOutputStream(response.getWriter());
}
// remove the last-modified field since caching otherwise does not work
@@ -629,14 +629,14 @@ public class YaCyDefaultServlet extends HttpServlet {
}
} else {
// Parse the satisfiable ranges
final List<InclusiveByteRange> ranges = InclusiveByteRange.satisfiableRanges(reqRanges, content_length);
final List<HttpByteRange> ranges = HttpByteRange.satisfiableRanges(reqRanges, content_length);
// if there are no satisfiable ranges, send 416 response
if (ranges == null || ranges.isEmpty()) {
this.writeHeaders(response, resource, content_length);
response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
response.setHeader(HttpHeader.CONTENT_RANGE.asString(),
InclusiveByteRange.to416HeaderRangeString(content_length));
response.setHeader(HEADER_CONTENT_RANGE,
HttpByteRange.to416HeaderRangeString(content_length));
resource.writeTo(out, 0, content_length);
out.close();
return;
@@ -645,11 +645,11 @@ public class YaCyDefaultServlet extends HttpServlet {
// if there is only a single valid range (must be satisfiable
// since were here now), send that range with a 216 response
if (ranges.size() == 1) {
final InclusiveByteRange singleSatisfiableRange = ranges.iterator().next();
final HttpByteRange singleSatisfiableRange = ranges.iterator().next();
final long singleLength = singleSatisfiableRange.getSize();
this.writeHeaders(response, resource, singleLength);
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
response.setHeader(HttpHeader.CONTENT_RANGE.asString(),
response.setHeader(HEADER_CONTENT_RANGE,
singleSatisfiableRange.toHeaderRangeString(content_length));
resource.writeTo(out, singleSatisfiableRange.getFirst(), singleLength);
out.close();
@@ -665,14 +665,14 @@ public class YaCyDefaultServlet extends HttpServlet {
if (mimetype == null) {
ConcurrentLog.warn("FILEHANDLER","YaCyDefaultServlet: Unknown mimetype for " + request.getRequestURI());
}
final MultiPartOutputStream multi = new MultiPartOutputStream(out);
final MultipartByteRangeOutputStream multi = new MultipartByteRangeOutputStream(out);
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
// If the request has a "Request-Range" header then we need to
// send an old style multipart/x-byteranges Content-Type. This
// keeps Netscape and acrobat happy. This is what Apache does.
String ctp;
if (request.getHeader(HttpHeader.REQUEST_RANGE.asString()) != null) {
if (request.getHeader(HEADER_REQUEST_RANGE) != null) {
ctp = "multipart/x-byteranges; boundary=";
} else {
ctp = "multipart/byteranges; boundary=";
@@ -686,7 +686,7 @@ public class YaCyDefaultServlet extends HttpServlet {
int length = 0;
final String[] header = new String[ranges.size()];
for (int i = 0; i < ranges.size(); i++) {
final InclusiveByteRange ibr = ranges.get(i);
final HttpByteRange ibr = ranges.get(i);
header[i] = ibr.toHeaderRangeString(content_length);
length +=
((i > 0) ? 2 : 0)
@@ -700,7 +700,7 @@ public class YaCyDefaultServlet extends HttpServlet {
response.setContentLength(length);
for (int i = 0; i < ranges.size(); i++) {
final InclusiveByteRange ibr = ranges.get(i);
final HttpByteRange ibr = ranges.get(i);
multi.startPart(mimetype, new String[]{HeaderFramework.CONTENT_RANGE + ": " + header[i]});
final long start = ibr.getFirst();
@@ -731,10 +731,10 @@ public class YaCyDefaultServlet extends HttpServlet {
}
/* ------------------------------------------------------------ */
protected void writeHeaders(final HttpServletResponse response, final Resource resource, final long count) {
protected void writeHeaders(final HttpServletResponse response, final ServletResource resource, final long count) {
if (response.getContentType() == null) {
final String extensionmime;
if ((extensionmime = this._mimeTypes.getMimeByExtension(resource.getName())) != null) {
if ((extensionmime = this._servletContext.getMimeType(resource.getName())) != null) {
response.setContentType(extensionmime);
}
}
@@ -1035,7 +1035,7 @@ public class YaCyDefaultServlet extends HttpServlet {
}
this.updateRespHeadersForImages(target, response);
final String mimeType = Classification.ext2mime(targetExt, MimeTypes.Type.TEXT_HTML.asString());
final String mimeType = Classification.ext2mime(targetExt, MIME_TEXT_HTML);
response.setContentType(mimeType);
response.setContentLength(result.length());
response.setStatus(HttpServletResponse.SC_OK);
@@ -1150,7 +1150,7 @@ public class YaCyDefaultServlet extends HttpServlet {
templatePatterns.put(SwitchboardConstants.GREETING_IMAGE_ALT, sb.getConfig(SwitchboardConstants.GREETING_IMAGE_ALT, ""));
templatePatterns.put("clientlanguage", localeSelection);
final String mimeType = Classification.ext2mime(targetExt, MimeTypes.Type.TEXT_HTML.asString());
final String mimeType = Classification.ext2mime(targetExt, MIME_TEXT_HTML);
InputStream fis;
final long fileSize = targetLocalizedFile.length();
@@ -1239,7 +1239,7 @@ public class YaCyDefaultServlet extends HttpServlet {
*/
private void writeInputStream(final HttpServletResponse response, final String targetExt, final InputStream inStream)
throws IOException {
final String mimeType = Classification.ext2mime(targetExt, MimeTypes.Type.TEXT_HTML.asString());
final String mimeType = Classification.ext2mime(targetExt, MIME_TEXT_HTML);
response.setContentType(mimeType);
response.setStatus(HttpServletResponse.SC_OK);
final byte[] buffer = new byte[4096];
@@ -1372,4 +1372,177 @@ public class YaCyDefaultServlet extends HttpServlet {
}
}
/** Join two servlet paths without depending on a container URI utility. */
private static String addPaths(final String first, final String second) {
if (first == null || first.isEmpty()) {
return second == null ? "" : second;
}
if (second == null || second.isEmpty()) {
return first;
}
final boolean firstEndsWithSlash = first.charAt(first.length() - 1) == '/';
final boolean secondStartsWithSlash = second.charAt(0) == '/';
if (firstEndsWithSlash && secondStartsWithSlash) {
return first + second.substring(1);
}
if (!firstEndsWithSlash && !secondStartsWithSlash) {
return first + '/' + second;
}
return first + second;
}
/** Equivalent of Jetty's WriterOutputStream default-charset behavior. */
private static final class CharacterOutputStream extends OutputStream {
private final Writer writer;
private final byte[] singleByte = new byte[1];
private CharacterOutputStream(final Writer writer) {
this.writer = writer;
}
@Override
public void write(final int value) throws IOException {
this.singleByte[0] = (byte) value;
this.write(this.singleByte);
}
@Override
public void write(final byte[] data, final int offset, final int length) throws IOException {
this.writer.write(new String(data, offset, length));
}
@Override
public void flush() throws IOException {
this.writer.flush();
}
@Override
public void close() throws IOException {
this.writer.close();
}
}
/** Inclusive byte range parsed from an HTTP Range header. */
private static final class HttpByteRange {
private final long first;
private final long last;
private HttpByteRange(final long first, final long last) {
this.first = first;
this.last = last;
}
private long getFirst() {
return this.first;
}
private long getLast() {
return this.last;
}
private long getSize() {
return this.last - this.first + 1;
}
private String toHeaderRangeString(final long size) {
return "bytes " + this.first + '-' + this.last + '/' + size;
}
private static String to416HeaderRangeString(final long size) {
return "bytes */" + size;
}
private static List<HttpByteRange> satisfiableRanges(
final Enumeration<String> headers, final long size) {
final List<HttpByteRange> ranges = new java.util.ArrayList<>();
while (headers != null && headers.hasMoreElements()) {
final String header = headers.nextElement();
if (header == null || !header.regionMatches(true, 0, "bytes=", 0, 6)) {
continue;
}
final String[] specifications = header.substring(6).split(",");
for (final String specification : specifications) {
final HttpByteRange range = parse(specification.trim(), size);
if (range != null) {
ranges.add(range);
}
}
}
return ranges;
}
private static HttpByteRange parse(final String specification, final long size) {
final int separator = specification.indexOf('-');
if (separator < 0 || size <= 0) {
return null;
}
try {
if (separator == 0) {
final long suffixLength = Long.parseLong(specification.substring(1).trim());
if (suffixLength <= 0) {
return null;
}
return new HttpByteRange(Math.max(0, size - suffixLength), size - 1);
}
final long first = Long.parseLong(specification.substring(0, separator).trim());
if (first < 0 || first >= size) {
return null;
}
final String lastText = specification.substring(separator + 1).trim();
final long last = lastText.isEmpty()
? size - 1
: Math.min(Long.parseLong(lastText), size - 1);
return last < first ? null : new HttpByteRange(first, last);
} catch (final NumberFormatException e) {
return null;
}
}
}
/** Minimal multipart writer for HTTP byte-range responses. */
private static final class MultipartByteRangeOutputStream extends FilterOutputStream {
private static final byte[] CRLF = {'\r', '\n'};
private final String boundary = "yacy-" + java.util.UUID.randomUUID().toString();
private boolean firstPart = true;
private MultipartByteRangeOutputStream(final OutputStream output) {
super(output);
}
private String getBoundary() {
return this.boundary;
}
private void startPart(final String contentType, final String[] headers) throws IOException {
if (!this.firstPart) {
this.out.write(CRLF);
}
this.firstPart = false;
this.writeAscii("--" + this.boundary + "\r\n");
if (contentType != null) {
this.writeAscii(HeaderFramework.CONTENT_TYPE + ": " + contentType + "\r\n");
}
if (headers != null) {
for (final String header : headers) {
this.writeAscii(header + "\r\n");
}
}
this.out.write(CRLF);
}
@Override
public void close() throws IOException {
this.out.write(CRLF);
this.writeAscii("--" + this.boundary + "--\r\n");
super.close();
}
private void writeAscii(final String value) throws IOException {
this.out.write(value.getBytes(StandardCharsets.ISO_8859_1));
}
}
}
@@ -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());
}
}
@@ -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.
*
+81
View File
@@ -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."
+135
View File
@@ -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();
}
}
+39
View File
@@ -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."